03. pointcut 작성
- -
이번 포스트에서는 advice를 어떤 target의 메서드 즉 join point에 적용 할 것인지 결정하기 위한 pointcut 작성에 대해 살펴보자.
Pointcut(포인트컷)
pointcut이란?
Pointcut은 advice를 적용할 target의 메서드를 결정하는 메서드 선정 알고리즘으로 @Before 등 애너테이션의 value 값이다. pointcut을 작성 할 때는 execution, within, bean 등 지정자와 함께 사용한다.
- execution: 메서드의 signature 기반으로 한 정교한 포인트컷 작성법으로 가장 많이 사용된다.
- @Before("execution(String *..SimpleBean.getName())")
- within: 빈 클래스 기반으로 포인트컷을 작성한다. 작성 방식은 execution의 class 부분과 동일하다.
- @Before("within(com..SimpleBeanImpl)")
- bean: 빈의 이름 기반으로 포인트컷을 작성한다.
- @Before("bean(pointcutTestBean)")
여기서는 execution 기반으로 포인트컷 작성법을 살펴보자.
execution을 이용한 매서드 패턴 지정
execution에 작성하는 메서드의 패턴은 아래 그림처럼 4개 부분으로 나뉜다.
다음은 각 부분에 대한 작성 방법과 사용되는 기호에 대한 설명이다.
구분 | 설명 | ||
return_type | 특징 | 메서드의 리턴 타입을 나타내며 필수 입력사항 | |
기호 | * | 타입에 무관 | |
! | 타입에 대한 부정 ex) !String, !void | ||
package + class | 특징 | 패키지를 포함하는 클래스이름으로 메서드가 선언된 클래스 또는 상위 타입 선택사항으로 생략 가능하며 생략하지 않을 경우 메서드 이름과 .로 연결 |
|
기호 | .. | 0개 이상의 하위 패키지 대체. 맨 처음에는 등장할 수 없음 | |
* | 0개 이상의 문자열 대체 | ||
method_name | 특징 | 메서드의 이름을 나타내며 필수 입력사항 | |
기호 | * | 0개 이상의 문자열을 대체 | |
parameter | 특징 | 메서드의 파라미터를 이름 없이 타입만 입력하며 선택사항 | |
기호 | * | 파라미터의 타입에 무관하게 1개의 파라미터 대체 | |
.. | 파라미터의 타입에 무관하게 0개 이상의 파라미터 대체 |
Spring의 AOP에서 pointcut을 작성할 때는 접근 제한자로 public만 사용 가능하다. interface 기반의 메섣드들은 어차피 public일 것이고 CGLib를 통해서 만들어지는 proxy도 public만 지원한다. 따라서 pointcut에서 접근 제한자는 의미가 없다.
return_type이나 parameter에 객체형을 사용할 때는 패키지 이름까지 명시해주는 것이 좋다. pointcut에 클래스 이름만 썼을 때는 중복되는 클래스를 구분하지 못하기 때문이다. 예를 들어 List라고 썼을 때 java.util.List인지 java.awt.List인지 구분할 수 없다. 물론 클래스 패스에서 유일한 이름이라면 상관 없지만 명확하게 하기 위해서는 패키지 이름까지 포함하는 것이 유리하다.
execution(public java.util.List<com.example.MyClass> *(..))
여러 개의 pointcut을 연결할 때는 &&, ||, !, and, or, not 등을 사용할 수 있다.
@Before("execution(* com..*(..)) || execution(* org..*(..))")
포인트컷 적용 예
빈을 추가하고 다양한 포인트컷이 주어질 때 어떤 메서드가 연결되는지 확인해 보자.
단위테스트를 통해서 빈의 메서드들을 호출해보자.
@SpringBootTest
@ActiveProfiles("dev")
@Slf4j
public class PointcutTest {
@Autowired
CalculatorBean cbean;
@Autowired
PointcutBean pbean;
@Test
public void methodCall(){
cbean.add(1, 2, 3);
cbean.divide(1, 2);
pbean.setName("hong");
pbean.getName() ;
pbean.print(List.of("hong", "jang"));
}
}
advice를 추가하고 pointcut을 다양하게 변경해보면서 어떤 메서드들이 연동되는지 고민해보자.
@Before("execution(String com.doding.aop.beans.*.get*())")
// @Before("execution(!void com.doding..*.*(..))")
// @Before("execution(* com.doding..*(*))")
// @Before("execution(* com.doding..*(String))")
// @Before("execution(* com.doding..*(java.util.List))")
public void pointcutTest(JoinPoint jp){
log.trace("pointcut 확인: {}", jp.getSignature());
}
기타
execution 이외의 지정자
pontcut을 작성하면서 지시자로 execution 이 외에도 within이나 bean, @annotation등을 사용할 수도 있다.
within은 실제 타겟 빈의 타입을 기준으로 해당 클래스, 인터페이스 내의 모든 메서드를 pointcut으로 지정할 수 있다. 유사하게 @within은 빈에 적용된 에너테이션을 기준으로 pointcut을 지정할 때 사용된다. 타입을 지정하는 세부적인 작성 방식은 execution의 class 부분과 동일하다.
@Before("within(com.doding.service.*)") // com.doding.service 소속 빈들의 모든 메서드
public void serviceBeans() {}
@Before("@within(org.springframework.stereotype.Service)") // @Service 타입 빈들의 모든 메서드
public void serviceBeans() {}
bean 은 스프링의 빈 이름을 이용하여 포인트컷을 지정할 때 사용된다. 따라서 빈의 구체적인 타입이나 클래스의 구조와 무관하게 빈의 이름만으로 pointcut을 지정한다.
@Before("bean(*Service)")
public void serviceBeans() {}
@annotation 지정하는 특정 애너테이션이 선언된 메서드에 대해서 advice를 지정할 때 사용할 수 있다.
@Before("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalMethods() {}
pointcut 재사용
만약 하나의 pointcut이 여러개의 advice에 적용될 수 있다면 pointcut을 저장해서 재사용할 수 있다. 이것을 named pointcut이라고 하고 이를 위해 @Pointcut 애너테이션이 사용된다. @Pointcut이 선언된 메서드의 이름은 pointcut id가 되고 advice를 선언할 때 이 pointcut id를 이용하면 된다. 사용시 주의 사항은 마치 메서드를 호출하는 형태로 사용해야 하며 다른 클래스에 선언된 pointcut id를 사용해야 하는 경우 패키지를 포함한 전체 경로명을 써줘야 한다.
package com.doding.some.aop;
public class PointCuts {
@Pointcut("execution(* com.doding.service.*.*(..))")
public void serviceLayerExecution() {
// 내용은 필요 없음
}
}
package com.doding.other.aop;
@Aspect
@Component
public class LoggingAspect {
@Before("com.doding.some.aop.Pointcuts.serviceLayerExecution()")
public void logBeforeServiceLayerExecution(JoinPoint joinPoint) {
// 타겟 동작 전에 실행 할 일 정리
}
@After("com.doding.some.aop.Pointcuts.serviceLayerExecution()")
public void logAfterServiceLayerExecution(JoinPoint joinPoint) {
// 타겟 동작 후에 실행 할 일 정리
}
}
pointcut 작성 시 주의사항
일반적으로 Pointcut 작성 시는 다음의 사항에 주의해야 한다.
- 정확한 범위 지정: 너무 넓은 범위의 Pointcut은 시스템 성능에 영향을 줄 수 있으며 전혀 예상치 못한 결과를 얻을 수 있다. 다음 표현식의 문제는 무엇일까?
@Before("execution(* *(int, ..))")
Spring이 관리하는 빈은 우리가 만든것 이외에도 어마어마하게 많고 그중에 어떤 빈의 어떤 메서드에 AOP가 적용되는지 알수 없다. 따라서 와일드카드를 남발하지 말고 적절한 패키지와 클래스를 이용한 제한은 반드시 필요하다.
- 중복 방지: 여러 pointcut이 같은 메서드에 매칭될 경우 예상치 못한 동작이 발생할 수 있다. 따라서 pointcut을 설계할 때는 중복되지 않게 해야 하고 어쩔 수 없이 중복되는 경우는 @Order를 이용해서 순서를 명시해주자.
- 동적 프록시의 이해: Spring의 AOP는 상속을 기반으로 동적 프록시를 생성한다. 따라서 final 클래스/메서드나 public 이외의 메서드는 AOP를 적용할 수 없다.
- 마지막으로 AOP가 적용되면 쉽게 코드의 동작을 파악하기가 어렵다. 해당 메서드만 분석해서는 답이 없기 때문이다. 따라서 작성자의 정신이 가장 말짱할 때 반드시 충분한 테스트를 하고 문서화를 해두어야 나중에 유지보수가 용이하게 된다.
@Test
@DisplayName("CommonAspect의 argumentLogging에 의해 호출 시 로그 출력")
public void addTest(){
int result = calculatorBean.add(1,2, 3);
Assertions.assertEquals(6, result);
}
'Spring Core > 03. AOP' 카테고리의 다른 글
05. Spring 내부의 AOP들 (0) | 2021.10.29 |
---|---|
04. advice의 타입 (0) | 2020.06.20 |
02. Aspect 작성과 동작 (0) | 2020.06.18 |
01. AOP 기본 컨셉 (0) | 2020.06.18 |
소중한 공감 감사합니다