Spring AOP - 포인트컷

조갱·2024년 7월 14일
0

스프링 강의

목록 보기
22/23

포인트컷 지시자

포인트컷 표현식

포인트컷 표현식은 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

메소드 실행 조인포인트를 매칭한다.
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();

패키지 매칭에서
. : 정확하게 해당 위치 패키지
.. : 그 패키지와 하위 패키지

execution 지시자는 부모 패키지도 허용한다.

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();

execution 지시자로 부모 패키지를 지정하면, 자식 클래스의 메소드는 사용하지 못한다.

// 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

within 지시자는 특정 타입 내의 조인 포인트들로 매칭을 제한한다.
execution 에서 타입 부분만 사용한다고 보면 된다.

within(선언타입)

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 표현식에 부모 클래스를 넣는 경우 - execution과 차이점

// 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

args 지시자는 인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭한다.
executionargs 부분만 사용한다고 보면 된다.

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를 사용하는 예제 - execution과 차이점

// 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 지시자는 단독으로 사용하면 안된다고 작성했다.
이유는 추후에 다른 지시자들과 함께 후술한다.

@target, @within

클래스의 어노테이션을 기준으로 AOP 적용 여부를 결정한다.

@target : 인스턴스의 모든 메소드를 조인 포인트로 적용한다.
@within : 해당 타입 내에 있는 메소드만 조인 포인트로 적용한다.

즉, @target은 부모클래스에 있는 메소드도 조인 포인트에 포함되며
@within 은 어노테이션이 적용된 클래스의 메소드만 조인 포인트에 포함된다.

args 와 마찬가지로, 단독으로 사용하면 안된다.

위와 같은 클래스 구조가 있다고 가정하고 예제 코드를 확인해보자.

Aspect

@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 Code

@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

@annotation : 주어진 어노테이션을 가지고 있는 메소드에 조인 포인트를 매칭
@args : 런타임 시점에 주어진 어노테이션을 갖고 있으면 AOP 적용

@annotation(hello.aop.member.annotation.MethodAop)
@args(hello.aop.member.annotation.MethodAop)

Aspect

@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();
    }
}

TestCode

@Test
void success() {
    log.info("memberService Proxy={}", memberService.getClass());
    memberService.hello("helloA");
}

실행 결과

[@annotation] String hello.aop.member.MemberService.hello(String)

bean

스프링에서만 사용되는 특수한 지시자
Spring Bean 이름으로 조인 포인트를 매칭한다.
* 패턴도 사용 가능하다.

Aspect

@Aspect
static class BeanAspect {
    @Around("bean(orderService) || bean(*Repository)")
    public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("[bean] {}", joinPoint.getSignature());
        return joinPoint.proceed();
    }
}

TestCode

@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

this : 스프링 빈 객체(프록시 객체)를 대상으로 하는 조인 포인트
target : Target 객체(프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트
* 을 사용할 수 있고, 부모 타입도 가능하다.

타입 하나를 정확하게 지정해야 한다.
this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

프록시 객체 생성 방식에 따른 차이

  • JDK 동적 프록시
    JDK 동적 프록시는 인터페이스를 가지고 프록시를 만든다.
    따라서, 프록시 객체는 구체 클래스의 존재 자체를 모른다.
  • CGLIB 프록시
    CGLIB 프록시는 구체 클래스를 상속받아 프록시를 만든다.
    구체 클래스의 존재와, 부모 클래스의 존재도 알고있다.

JDK 동적 프록시를 사용하는 경우

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가 적용된다.

CGLIB 프록시를 사용하는 경우

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 지시자 사용시 주의사항

args , @args , @target 은 실제 객체 인스턴스가 생성되고 실행될 때 어드바이스 적용 여부를 확인할 수 있다.

실행 시점에 포인트컷 적용 여부도, 프록시가 적용되어야 판단할 수 있다.
프록시는 스프링 어플리케이션이 실행되는 시점 (스프링 컨테이너가 만들어지는 시점)에 적용된다.

즉,
1. args , @args , @target 지시자만 있다면
2. 실행 시점에 포인트컷 적용 여부를 판단하기 위해
3. 스프링은 모든 스프링 빈에 AOP를 적용하려고 한다. (스프링 자체 bean에도.)
4. 이 때 final 클래스를 만나면, 프록시를 생성하지 못하고 에러가 발생한다.

따라서 이러한 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 한다.

매개변수 전달

this, target, args,@target, @within, @annotation, @args 지시자를 사용하면, 어드바이스에 매개변수를 전달할 수 있다.

  • 포인트컷의 이름과 매개변수의 이름을 맞추어야 한다.
  • 타입이 메소드 (어드바이스)에 지정한 타입으로 제한된다.

Aspect

@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() 로 해당 애노테이션의 값을 출력하는 모습을 확인할 수 있다.

TestCode

@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
profile
A fast learner.

0개의 댓글