포인트컷 표현식은 AspectJ Pointcut Expression 을 줄여서 부르는 말이다.
AspectJ는 Pointcut을 편리하게 사용하기 위해 특별한 표현식을 제공한다.
예) @Pointcut("execution(* hello.aop.order..*(..))")
포인트컷 표현식은 execution
같은 포인트컷 지시자(Pointcut Designator)로 시작한다.
execution
: 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.within
: 특정 타입 내의 조인 포인트를 매칭한다.args
: 인자가 주어진 타입의 인스턴스인 조인 포인트this
: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트target
: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트@target
: 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트@within
: 주어진 애노테이션이 있는 타입 내 조인 포인트@annotation
: 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭@args
: 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트bean
: 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다.
예제 코드를 보기 전에, 위 클래스 다이어그램을 이해하자.
MemberService 인터페이스가 있고, 멤버로 String 을 반환하는 hello() 메소드가 있다.
그 인터페이스를 구현하는 MemberServiceImpl에는 @ClassAop 어노테이션이 있다.
MemberServiceImpl 는 String hello()를 구현하며, @MethodAop("test") 어노테이션이 있다.
그 외에 자체적으로 String 타입의 internal() 메소드가 있다.
@Slf4j
public class ExecutionTest {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
Method helloMethod;
@BeforeEach
public void init() throws NoSuchMethodException {
helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
}
@Test
void printMethod() {
log.info("helloMethod={}", helloMethod);
}
}
이 클래스 안에 계속해서 @Test 를 추가해나간다.
메소드 실행 조인포인트를 매칭한다.
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
- ? 로 표현된 항목은 생략 가능
- * 표현 사용 가능
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
접근제어자? : public
반환타입 : String
선언타입? : hello.aop.member.MemberServiceImpl
메소드이름 : hello
파라미터 : String 1개
예외? : 생략 (없음)
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
접근제어자? : 생략
반환타입 : * (모두)
선언타입? : 생략
메소드 이름 : * (모두)
파라미터 : 모든 타입의 파라미터, 개수 상관 없음.
예외? : 생략
pointcut.setExpression("execution(* hello(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("execution(* hel*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("execution(* *el*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("execution(* nono(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("execution(* hello.aop.*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
pointcut.setExpression("execution(* hello.aop.member..*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("execution(* hello.aop..*.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
패키지 매칭에서
.
: 정확하게 해당 위치 패키지
..
: 그 패키지와 하위 패키지
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
// MemberServiceImpl 구체클레스를 지정하면, internal 메소드를 사용할 수 있다.
pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isTrue();
// MemberService 인터페이스를 지정하면, 찾을 수는 있으나 internal 메소드는 사용할 수 없다.
// internal 메소드는 구체 클래스인 MemberServiceImpl 이 가지고있기 때문.
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
// String 타입의 파라미터 허용
// (String)
pointcut.setExpression("execution(* *(String))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
// 파라미터가 없어야 함
// ()
pointcut.setExpression("execution(* *())");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
// 정확히 하나의 파라미터 허용, 모든 타입 허용
// (XXX)
pointcut.setExpression("execution(* *(*))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
// 숫자와 무관하게 모든 파라미터, 모든 타입 허용, 파라미터가 없어도 됨
// (), (XXX), (XXX, XXX)
pointcut.setExpression("execution(* *(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
// String 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용
// (String), (String, XXX), (String, XXX, XXX)
pointcut.setExpression("execution(* *(String, ..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
within
지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다.
execution
에서 타입 부분만 사용한다고 보면 된다.within(선언타입)
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("within(hello.aop.member.*Service*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
pointcut.setExpression("within(hello.aop..*)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
// within 표현식을 사용하면 부모 클래스를 지정했을 떄 찾을 수 없다.
pointcut.setExpression("within(hello.aop.member.MemberService)");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
// execution 표현식은 부모 클래스를 지정했을 때 찾을 수 있다.
pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
args
지시자는 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭한다.
execution
의args
부분만 사용한다고 보면 된다.args 지시자는 단독으로 사용하면 안된다.
파라미터 바인딩을 위한 목적으로 주로 사용된다.
assertThat(pointcut("args(String)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(Object)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args()").matches(helloMethod, MemberServiceImpl.class)).isFalse();
assertThat(pointcut("args(..)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(*)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(String,..)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
// Args
assertThat(pointcut("args(String)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(java.io.Serializable)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("args(Object)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
// Execution, 아래 2개는 실패한다.
assertThat(pointcut("execution(* *(String))").matches(helloMethod, MemberServiceImpl.class)).isTrue();
assertThat(pointcut("execution(* *(java.io.Serializable))").matches(helloMethod, MemberServiceImpl.class)).isFalse();
assertThat(pointcut("execution(* *(Object))").matches(helloMethod, MemberServiceImpl.class)).isFalse();
Args : 런타임 시점에 동적으로
포인트컷 매칭 여부를 판단한다. 따라서 부모 타입도 가능
Execution : 컴파일 시점에 정적으로 포인트컷 매칭 여부를 판단한다. 따라서 타입이 정확해야 한다.
런타임 시점에 동적으로
를 강조한 이유가 있다.
이것 때문에 위에서 args 지시자는 단독으로 사용하면 안된다고 작성했다.
이유는 추후에 다른 지시자들과 함께 후술한다.
클래스의 어노테이션을 기준으로 AOP 적용 여부를 결정한다.
@target
: 인스턴스의 모든 메소드를 조인 포인트로 적용한다.
@within
: 해당 타입 내에 있는 메소드만 조인 포인트로 적용한다.즉,
@target
은 부모클래스에 있는 메소드도 조인 포인트에 포함되며
@within
은 어노테이션이 적용된 클래스의 메소드만 조인 포인트에 포함된다.args 와 마찬가지로, 단독으로 사용하면 안된다.
위와 같은 클래스 구조가 있다고 가정하고 예제 코드를 확인해보자.
@Slf4j
@Aspect
static class AtTargetAtWithinAspect {
// @target: 인스턴스의 모든 메소드를 조인 포인트에 포함 -> 부모 타입의 메소드도 적용
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")
public Object atTarget(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@target] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
// @within: 어노테이션이 적용된 클래스의 메소드만 조인 포인트에 포함 -> 부모 타입의 메소드는 적용 X
@Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop)")
public Object atWithin(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@within] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
포인트컷 표현식을 보면, @target
과 @within
뿐만 아니라 execution
도 함께 적용된 것을 확인할 수 있다.
이는 args
지시자와 마찬가지로, @target
,@within
도 런타임에 동적으로 적용되기 때문이다.
뒤에서 자세하게 다시 설명한다.
@Test
void success() {
log.info("child Proxy={}", child.getClass());
child.childMethod(); //부모, 자식 모두 있는 메서드
child.parentMethod(); //부모 클래스만 있는 메서드
}
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()
@target
은 child 메소드에 존재하는 parentMethod()와 childMethod() 에 Aop를 적용한다.
@within
은 @ClassAop 어노테이션이 적용된 Child 클래스의 멤버인 childMethod() 에만 Aop를 적용한다.
@annotation
: 주어진 어노테이션을 가지고 있는 메소드에 조인 포인트를 매칭
@args
: 런타임 시점에 주어진 어노테이션을 갖고 있으면 AOP 적용
@annotation(hello.aop.member.annotation.MethodAop)
@args(hello.aop.member.annotation.MethodAop)
@Slf4j
@Aspect
static class AtAnnotationAspect {
@Around("@annotation(hello.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[@annotation] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Test
void success() {
log.info("memberService Proxy={}", memberService.getClass());
memberService.hello("helloA");
}
[@annotation] String hello.aop.member.MemberService.hello(String)
스프링에서만 사용되는 특수한 지시자
Spring Bean 이름으로 조인 포인트를 매칭한다.
* 패턴도 사용 가능하다.
@Aspect
static class BeanAspect {
@Around("bean(orderService) || bean(*Repository)")
public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("[bean] {}", joinPoint.getSignature());
return joinPoint.proceed();
}
}
@Test
void success() {
orderService.orderItem("itemA");
}
[bean] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[bean] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행
this
: 스프링 빈 객체(프록시 객체)를 대상으로 하는 조인 포인트
target
: Target 객체(프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
* 을 사용할 수 있고, 부모 타입도 가능하다.타입 하나를 정확하게 지정해야 한다.
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)
MemberService 인터페이스 지정
this(hello.aop.member.MemberService)
: proxy 객체 - AOP가 적용된다. target(hello.aop.member.MemberService)
: target 객체 - AOP가 적용된다.
MemberServiceImpl 구체 클래스 지정
this(hello.aop.member.MemberServiceImpl)
: proxy 객체 - AOP 적용 대상이 아니다.
target(hello.aop.member.MemberServiceImpl)
: target 객체 - AOP가 적용된다.
MemberService 인터페이스 지정
this(hello.aop.member.MemberService)
: proxy 객체 - AOP가 적용된다.
target(hello.aop.member.MemberService)
: target 객체 - AOP가 적용된다.
MemberServiceImpl 구체 클래스 지정
this(hello.aop.member.MemberServiceImpl)
: proxy 객체 - AOP가 적용된다.
target(hello.aop.member.MemberServiceImpl)
: target 객체 - AOP가 적용된다.
args
, @args
, @target
은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.
실행 시점에 포인트컷 적용 여부도, 프록시가 적용되어야 판단할 수 있다.
프록시는 스프링 어플리케이션이 실행되는 시점 (스프링 컨테이너가 만들어지는 시점)에 적용된다.
즉,
1. args
, @args
, @target
지시자만 있다면
2. 실행 시점에 포인트컷 적용 여부를 판단하기 위해
3. 스프링은 모든 스프링 빈에 AOP를 적용하려고 한다. (스프링 자체 bean에도.)
4. 이 때 final
클래스를 만나면, 프록시를 생성하지 못하고 에러가 발생한다.
따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.
this, target, args,@target, @within, @annotation, @args 지시자를 사용하면, 어드바이스에 매개변수를 전달할 수 있다.
- 포인트컷의 이름과 매개변수의 이름을 맞추어야 한다.
- 타입이 메소드 (어드바이스)에 지정한 타입으로 제한된다.
@Slf4j
@Aspect
static class ParameterAspect {
@Pointcut("execution(* hello.aop.member..*.*(..))")
private void allMember() {}
@Around("allMember()")
public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
Object arg1 = joinPoint.getArgs()[0];
log.info("[logArgs1]{}, arg={}", joinPoint.getSignature(), arg1);
return joinPoint.proceed();
}
@Around("allMember() && args(arg,..)")
public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
log.info("[logArgs2]{}, arg={}", joinPoint.getSignature(), arg);
return joinPoint.proceed();
}
@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
log.info("[logArgs3] arg={}", arg);
}
@Before("allMember() && this(obj)")
public void thisArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
@Before("allMember() && target(obj)")
public void targetArgs(JoinPoint joinPoint, MemberService obj) {
log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
}
@Before("allMember() && @target(annotation)")
public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @within(annotation)")
public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
}
@Before("allMember() && @annotation(annotation)")
public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation.value());
}
}
logArgs1
: joinPoint.getArgs()[0]
와 같이 매개변수를 전달 받는다.
logArgs2
: args(arg,..)
와 같이 매개변수를 전달 받는다.
logArgs3
: @Before
를 사용한 축약 버전이다. 추가로 타입을 String
으로 제한했다.
this
: 프록시 객체를 전달 받는다.
target
: 실제 대상 객체를 전달 받는다.
@target
, @within
: 타입의 애노테이션을 전달 받는다.
@annotation
: 메서드의 애노테이션을 전달 받는다. 여기서는 annotation.value()
로 해당 애노테이션의 값을 출력하는 모습을 확인할 수 있다.
@Test
void success() {
log.info("memberService Proxy={}", memberService.getClass());
memberService.hello("helloA");
}
memberService Proxy=class hello.aop.member.MemberServiceImpl$$EnhancerBySpringCGLIB$$82
[logArgs1]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA
[logArgs2]String hello.aop.member.MemberServiceImpl.hello(String), arg=helloA
[logArgs3] arg=helloA
[this]String hello.aop.member.MemberServiceImpl.hello(String), obj=class hello.aop.member.MemberServiceImpl$$EnhancerBySpringCGLIB$$8
[target]String hello.aop.member.MemberServiceImpl.hello(String), obj=class hello.aop.member.MemberServiceImpl
[@target]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
[@within]String hello.aop.member.MemberServiceImpl.hello(String), obj=@hello.aop.member.annotation.ClassAop()
[@annotation]String hello.aop.member.MemberServiceImpl.hello(String), annotationValue=test value