AOP 포인트컷 1

바그다드·2023년 10월 11일
0

지난 포스팅에서 AOP 어드바이스의 종류에 대해서 알아보았다.

타겟 메서드의 전후로 작용하는 @Around
타겟 호출 전에 작용하는 @Before
타겟 메서드가 정상적으로 완료되었을 때 작용하는 @AfterReturning

  • returning 속성의 타입과 타겟의 리턴 타입이 일치해야 함
    타겟 메서드에서 예외가 발생했을 때 작용하는 @AfterThrowing
  • throwing 속성의 타입과 타겟의 리턴 타입이 일치해야 함
    타겟 메서드의 성공 여부에 관계없이 작용하는 @After

@Around는 JoinPoint를 사용하는 다른 어드바이스와 달리 ProceedingJoinPoin를 사용하고, 개발자가 직접 proceed()메서드를 호출해야 타겟이 호출이 된다.

여기서 발생할 수 있는 문제 때문에 @Around이외의 어드바이스가 생겼는데, 그 이유는

  1. @Around이외의 어드바이스는 따로 타겟을 호출하는 로직이 없어도 자동으로 타겟을 호출한다는 것
  2. 개발자의 의도가 뚜렷하게 드러난다는 점
    • @Around의 경우 메서드 전후로 작용을 하기 때문에 이 어드바이스가 어떤 기능을 하는지, 어떤 의도로 어드바이스를 적용을 한 것인지 잘 드러나지 않을 수 있다.
      반면 @Before의 경우 타겟 호출 전에 작동을 하는 어드바이스라는 것을 보다 쉽게 알 수 있기 때문에 개발자의 의도가 보다 명확하게 드러난다.

어드바이스에 대해 알아보며 각 어드바이스에 포인트컷이 들어가는 것을 확인했는데, 이번 포스팅에서는 이 포인트컷과 포인트컷 지시자에 대해 알아보려고 한다.

포인트컷 지시자

에스팩트J에서는 포인트컷을 편하게 표현하기 위해 표현식을 제공한다. 아래의 코드를 보자.

@Pointcut("execution(* hello.aop.order..*(..))")

위의 코드에서 execution 같은 것을 포인트컷 지시자(PointCut Designator), PCD라고 한다.

포인트컷 지시자 종류

지시자설명
execution메서드 실행 조인 포인트를 매칭
가장 자주 사용하고 기능도 복잡
within특정 타입 내의 조인 포인트를 매칭
args타겟 파라미터가 지정한 타입의 인스턴스인 조인 포인트
this스프링 빈(스프링 AOP 프록시)을 대상으로 하는 조인 포인트
target타겟을 대상으로 하는 조인 포인트
@target타겟에 지정한 타입의 어노테이션이 있는 조인 포인트
@within지정한 어노테이션이 있는 타입 내 조인 포인트
@annotation메서드에 주어진 어노테이션을 가지고 있는 조인 포인트
@args전달된 실제 파라미터의 런타임 타입이 주어진 타입의 어노테이션인 조인 포인트
bean스프링 전용 포인트컷 지시자로, 빈의 이름으로 포인트컷 지정

코드로 확인해보자.

예제

ClassAop 어노테이션 생성

  • 클래스 단위로 붙이는 어노테이션
@Target(ElementType.TYPE)
// @Retention : 
// 
// 
// 
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
  • @Target
    해당 어노테이션이 부착될 수 있는 타입을 지정하는 어노테이션
    여기서 타입이란, 클래스, 메서드, 생성자 등을 뜻함
  • @Retention
    어노테이션의 라이프 사이클을 설정하는 어노테이션
    RetentionPolicy.Source : .java파일일 때까지만 어노테이션이 남아있고, 컴파일 되면 사라짐
    RetentionPolicy.Class : .class파일까지는 어노테이션이 남아있고, 런타임 시점에 클래스로더가 클래스를 읽어오면 사라짐
    RetentionPolicy.Runtime : 런타임 시점에도 어노테이션이 남아있음

MethodAop 어노테이션 생성

  • 메서드 단위로 붙이는 어노테이션
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
    String value();
}
  • 어노테이션도 값을 가질 수 있음

MemberService 인터페이스 생성

public interface MemberService {
    String hello(String param);
}

구현체 생성

@ClassAop
// AOP는 스프링 빈을 대상으로 작동하므로 컴포넌트 스캔
@Component
public class MemberServiceImpl implements MemberService{
    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }

    public String internal(String param) {
        return "ok";
    }
}

테스트 코드 생성

@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);
        
    }
}
  • 테스트 결과를 보면

    위와 같은 로그가 남는 것을 확인할 수 있는데, 앞서 사용했던 execution의 경우 이 메서드 정보를 이용해 포인트컷 대상을 식별한다.

(*) - 이게 붙은것은 실무에서 사용될법한 PCD임

1. execution(*)

가장 많이 사용되는 PCD로 메서드 실행 조인 포인트를 매칭한다.

  • 문법
execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
  • ?는 생략 가능한 부분을 뜻함
  • * 같은 패턴을 사용할 수 있다.

패턴

패턴설명

*

모든 값과 매칭
* 만 사용하면 모든 글자를 대체
글자와 함께 사용하면 일부만 대체

.

패키지 지정에 사용
정확하게 해당 위치의 패키지

..

패키지 지정에 사용
해당 위치의 패키지와 그 하위 패키지 모두 포함
0파라미터 부분에서 사용
아무런 파라미터가 들어오지 않는다는 뜻

*

파라미터 부분에서 사용
모든 타입이 들어올 수 있다는 뜻
하나의 파라미터만 대체

..

파라미터 부분에서 사용
아무런 값이 들어오지 않거나, 하나만 들어오거나,
여러개가 들어올 수 있다는 뜻

위에서 생성한 예제를 바탕으로 테스트를 통해 사용법을 알아보자.

  • 가장 정확한 매칭 표현식
pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
pointcut.setExpression("execution(* *(..))"); // 모든 타입 매칭
pointcut.setExpression("execution(* *hello*(..))"); // 이름으로 매칭
pointcut.setExpression("execution(* nono(..))"); // 매칭 실패
pointcut.setExpression("execution(* hello.aop.member.*.*(..))"); // 이름으로 매칭
pointcut.setExpression("execution(* hello.aop..*.*(..))"); // 하위 패키지 포함
assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();

2. within

포인트컷으로 지정한 타입으로 조인포인트를 매칭하는 PCD
부모 타입도 허용하는 execution과 달리 타겟의 타입과 정확히 매칭이 되어야 한다.

코드로 사용법에 대해 알아보자.

// 가장 정확히 매칭
pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
// *을 이용해 매칭
pointcut.setExpression("within(hello.aop.member.*Service*)");
// aop하위 패키지의 모든 타입과 매칭
pointcut.setExpression("within(hello.aop..*)");
// 부모로 매칭을 시도해 실패함
pointcut.setExpression("within(hello.aop.member.MemberService)");

3. Args

파라미터가 지정한 타입과 매칭되는 조인포인트를 매칭

  • 파라미터 타입이 정확히 매칭되어야 하는 execution과는 달리 부모 타입도 지원

코드로 확인해보자.

// hello(String)과 매칭
assertThat(pointcut("args(String)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
// 부모 타입인 Object로도 매칭
assertThat(pointcut("args(Object)").matches(helloMethod, MemberServiceImpl.class)).isTrue();
// 패턴은 execution과 일치
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 차이

  • execution(* *(java.io.Serializable)) : 메서드의 시그니처로 판단(정적)
    정적으로 클래스에 선언된 정보만을 가지고 판단

  • args(java.io.Serializable) : 런타임에 전달된 인수로 판단(동적)
    실제 파라미터로 넘어온 객체 인스턴스를 이용해 판단

    @Test
    void argsVsExecution() {
        // 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
        assertThat(pointcut("execution(* *(String))")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
        // false 반환
        assertThat(pointcut("execution(* *(java.io.Serializable))")
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
        // false 반환
        assertThat(pointcut("execution(* *(Object))")
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }

4. @Target과 @Within

@Target과 @Within은 클래스에 지정한 어노테이션이 붙어있는 타겟을 매칭하는 PCD이다.

둘의 차이는
@Target의 경우 인스턴스의 모든 메서드를 조인 포인트로 적용한다.
자신뿐 아니라 부모 클래스의 메서드까지 모두 어드바이스를 적용한다.

@Within의 경우 해당 타입에 있는 메서드에만 조인 포인트로 적용한다.
자시 자신에게 정의된 메서드에만 어드바이스를 적용한다.

@Slf4j
@Import({AtTargetAtWithinTest.Config.class})
@SpringBootTest
public class AtTargetAtWithinTest {
    @Autowired
    Child child;
    @Test
    void success() {
        log.info("child Proxy={}", child.getClass());
        child.childMethod(); //부모, 자식 모두 있는 메서드
        child.parentMethod(); //부모 클래스만 있는 메서드
    }
    
    // Parent, Child 스프링 빈으로 등록
    static class Config {
        @Bean
        public Parent parent() {
            return new Parent();
        }
        @Bean
        public Child child() {
            return new Child();
        }
        @Bean
        public AtTargetAtWithinAspect atTargetAtWithinAspect() {
            return new AtTargetAtWithinAspect();
        }
    }
    
    // 부모 클래스
    static class Parent {
        public void parentMethod(){} //부모에만 있는 메서드
    }
    
    // 자식 클래스
    @ClassAop
    static class Child extends Parent {
        public void childMethod(){}
    }
    
    // AOP
    @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: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용되지 않음
                @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();
                }
    }
}

부모 클래스인 Parent와 자식 클래스 Child를 생성하고 스프링 빈으로 등록해줬다.
자식 클래스인 Child에는 @ClassAop 어노테이션이 붙어 있으며
@Aspect에서 생성한 atTarget(), atWithin() 어드바이스 모두 @ClassAop를 포인트컷으로 설정하였다.

테스트 결과를 확인해보면,

@target은 자기 자신의 메서드와 부모 메서드 모두 호출하는 것을 확인할 수 있고,
@within은 자기 자신의 메서드만 호출하는 것을 확인할 수 있다.

args, @args, @target은 단독으로 사용하면 안된다.

args, @args, @target는 실행될 때 어드바이스 적용 여부를 확인하는데, 포인트컷 적용 여부는 프록시가 있어야 판단을 할 수 있다.
그런데 이 프록시는 스프링 컨테이너가 생성되는 어플리케이션 로딩 시점에 생성된다.
따라서 args, @args, @target 같은 포인트컷이 있으면 스프링에서는 모든 빈에 AOP를 적용하려고 한다. 프록시가 없으면 포인트컷 적용 여부를 판단할 수 없기 때문이다.
하지만 final이 붙은 빈의 경우 프록시를 생성하는 과정에서 오류가 발생할 수 있다.(cglib의 경우 상속을 이용해 프록시를 생성하기 때문이다.)
따라서 이런 지시자의 경우 예제에서처럼 프록시 적용 대상을 축소하는 지시자와 함께 사용해야 한다.

5. @annotation(*)

메서드가 지정한 어노테이션을 가지고 있으면 매칭

코드로 확인해보자.

// 생략
@Component
public class MemberServiceImpl implements MemberService{
    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }
    
// 생략
  • hello()는 @MethodAop라는 어노테이션을 가지고 있다.

  • 테스트 코드

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

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

    @Slf4j
    @Aspect
    static class AtAnnotationAspect {
    
		// @MethodAop를 포인트컷으로 지정
        @Around("@annotation(hello.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

}

결과를 확인해보면

지정한 어드바이스가 적용된 것을 확인할 수 있다.

6. @args

전달된 실제 파라미터의 런타임 타입이 지정한 어노테이션을 가지고 있을 경우 매칭
예를 들어 @Method를 포인트컷으로 지정했을 때 String타입 파라미터를 전달받았는데, String내부에 @Method 어노테이션을 가지고 있을 경우 매칭한다.

잘 사용되지 않으니 그냥 참고만 하자.

7. bean

스프링에서만 지원하는 PCD, 말 그대로 빈 이름으로 지정한다.
* 같은 패턴을 사용할 수 있다.

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {

    @Autowired
    OrderService orderService;

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Aspect
    static class BeanAspect {
        @Around("bean(orderService) || bean(*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}
  • OrderService와 정확히 일치하는 빈과, 이름 중 Repository를 포함하는 빈을 타겟으로 설정한다.

매개변수 전달 받기

다음의 포인트컷 표현식은 어드바이스에 매개변수를 전달할 수 있다.
this, target, args, @target, @within, @annotation, @args

코드로 확인해보자.

@Slf4j
@Import(ParameterTest.ParameterAspect.class)
@SpringBootTest
public class ParameterTest {

    @Autowired
    MemberService memberService;
	
    // 테스트 코드
    @Test
    void success() {
        log.info("memberService Proxy={}", memberService.getClass());
        memberService.hello("helloA");
    }

    @Slf4j
    @Aspect
    static class ParameterAspect {

        @Pointcut("execution(* hello.aop.member..*.*(..))")
        private void allMember() {
        }
		
        // JoinPoint를 이용해 인수 전달 받기
        @Around("allMember()")
        public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
            Object arg1 = joinPoint.getArgs()[0];
            log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), arg1);
            return joinPoint.proceed();
        }
		
        
        // 표현식과 메서드 파라미터를 매칭시켜 전달 받기
        @Around("allMember() && args(arg,..)")
        public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
            log.info("[logArgs1]{}, args={}", joinPoint.getSignature(), arg);
            return joinPoint.proceed();
        }
		
        // @Before로 축약함
        @Before("allMember() && args(arg,..)")
        public void logArgs3(String arg) {
            log.info("[logArgs3] arg={}", arg);
        }

        // this : 프록시를 대상으로 포인트컷을 매칭
        // 프록시를 전달받음
        @Before("allMember() && this(obj)")
        public void thisArgs(JoinPoint joinPoint, MemberService obj) {
            log.info("[this]{}, obj={}", joinPoint.getSignature(), obj.getClass());
        }

        // target : 실제 타겟을 대상으로 포인트컷을 매칭
        // 타겟 객체를 전달받음
        @Before("allMember() && target(obj)")
        public void targetArgs(JoinPoint joinPoint, MemberService obj) {
            log.info("[target]{}, obj={}", joinPoint.getSignature(), obj.getClass());
        }

        // @target : 타입이 지정한 어노테이션을 가지고 있을 경우 매칭(클래스 단위)
        // 지정한 타입의 어노테이션을 전달받음
        @Before("allMember() && @target(annotation)")
        public void atTarget(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
        }

        // @within : 타입이 지정한 어노테이션을 가지고 있을 경우 매칭(클래스 단위)
        // 지정한 타입의 어노테이션을 전달받음
        @Before("allMember() && @within(annotation)")
        public void atWithin(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
        }

        // @annotation : 메서드가 지정한 어노테이션을 가지고 있을 경우 매칭(메서드 단위)
        // 지정한 타입의 어노테이션을 전달받음
        // 어노테이션이 가지고 있는 값을 꺼낼 수 있음
        @Before("allMember() && @annotation(annotation)")
        public void atAnnotation(JoinPoint joinPoint, MethodAop annotation) {
            log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation.value());
        }

    }
}

테스트 결과를 확인해보면

각 지시자에 맞는 인자를 전달받은 것을 확인할 수 있다.

그런데 여기서 this와 target을 사용할 때 문제가 발생할 수도 있다.
this는 프록시 객체를, target은 실제 대상 객체를 인자로 전달받는다는 것까지는 구분이 되는데, JDK 동적프록시와 CGLIB 둘 중 어떤 기술을 사용해 프록시를 생성하느냐에 따라 의도하지 않는 결과가 발생할 수 있다.

this vs target(사용시 주의)

앞서 말했듯이
this는 프록시(스프링 빈)를 대상으로 하는 조인 포인트
target은 타겟(프록시가 가르키는 대상)을 대상으로 하는 조인포인트이다.

* 과 같은 패턴을 사용할 수 없다.
부모 타입을 지원한다.

언뜻 봐서는 두 개의 지시자에서 헷갈릴만한 요소가 뭔지 떠오르질 않는다.
하지만 프록시 방식에 따라서 결과가 달라지게 된다.

JDK는 기본적으로 인터페이스를 필수로 하고, 인터페이스를 구현한 프록시를 생성한다.
반면 CGLIB는 인터페이스가 있든 없든 구현 클래스를 상속받아 프록시를 생성한다.

그림으로 보자면 아래와 같다

포인트컷 지정에 따라 어떤 차이가 있는지 확인해보자.

MemberService(인터페이스)를 포인트컷으로 지정했을 때

  1. this(hello.aop.member.MemberService) - 프록시를 대상으로 AOP 적용여부를 판단
    • 이 프록시의 경우 MemberService(부모)를 구현했기 떄문에 AOP가 적용된다.
  2. target(hello.aop.member.MemberService) - target을 대상으로 AOP 적용여부를 판단
    • MemberServiceImple의 경우 MemberService를 구현하고 있으므로 AOP가 적용된다.

MemberServiceImpl(구체 클래스)을 포인트컷으로 지정했을 때

  1. this(hello.aop.member.MemberServiceImpl) - 프록시를 대상으로 AOP 적용여부를 판단

    • 스프링 빈에 등록된 프록시의 경우 MemberService(부모)를 구현한 구현체이고,
      MemberServiceImpl 또한 MemberService를 구현한 구현체이다.
      따라서 프록시와 MemberServiceImpl은 상속관계(또는 부모 자식 관계)가 아니므로 AOP 적용대상이 아니다.
  2. target(hello.aop.member.MemberServiceImpl) - target을 대상으로 AOP 적용여부를 판단
    - 포인트컷과 타겟의 타입이 동일하므로 AOP 적용 대상이 된다.

MemberService(인터페이스)를 포인트컷으로 지정했을 때

  1. this(hello.aop.member.MemberService) - 프록시를 대상으로 AOP 적용여부를 판단
    • 프록시가 MemberServiceImple을 상속받지만, MemberServiceImple는 MemberService를 구현하고 있으므로 AOP가 적용된다.
  2. target(hello.aop.member.MemberService) - target을 대상으로 AOP 적용여부를 판단
    • MemberServiceImple의 경우 MemberService를 구현하고 있으므로 AOP가 적용된다.

MemberServiceImpl(구체 클래스)을 포인트컷으로 지정했을 때

  1. this(hello.aop.member.MemberServiceImpl) - 프록시를 대상으로 AOP 적용여부를 판단
    • 프록시가 MemberServiceImpl을 상속받아 생성되므로 AOP가 적용된다.
  2. target(hello.aop.member.MemberServiceImpl) - target을 대상으로 AOP 적용여부를 판단
    • 포인트컷과 타겟의 타입이 동일하므로 AOP 적용 대상이 된다.

그런데 기본적으로 스프링은 CGLIB을 이용해 프록시를 생성하기 때문에 따로 설정을 변경하지 않으면 이런 문제를 접할 일은 별로 없을 것이다.

다만 만약 JDK 동적 프록시를 사용할 필요가 있다면 application.properties에 다음 설정을 활용하자.

# 기본 설정값(CGLIB 사용)
#spring.aop.proxy-target-class=true
# JDK 동적 프록시 사용
#spring.aop.proxy-target-class=false

Test에서 properties 임시로 사용하기

일일이 properties 파일을 바꾸기 번거롭다면 아래와 같은 방법으로 설정값을 임시로 변경할 수 있다.

@SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시 사용

정리

이번 포스팅에서는 여러종류의 포인트컷 지시자에 대해서 알아보았다.

포인트컷 지시자(PointCut Designator, PCD)에는 다음과 같은 종류가 있다.

  1. execution

    • 가장 많이 쓰이고 많은 기능을 제공하는만큼 복잡하다.
    • 접근제어자? 반환타입 선언타입.메서드(파라미터) 예외?
      로 구성되며 ?는 생략이 가능하다.
  2. within

    • 지정한 타입으로 매칭하는 포인트컷
    • 부모타입을 지원하지 않고 정확한 타입을 지정해야 한다.
  3. args

    • 넘어오는 파라미터의 런타임 타입이 포인트컷에서 지정한 타입인 조인포인트
    • 부모타입을 지원한다.
  • 여기서 execution과 args의 파라미터를 확인하는 방법이 서로 달랐는데,
    execution은 단순히 타겟 메서드에 명시되어 있는 타입과 일치하는지 정적으로 검증하고,
    args는 실제로 넘어오는 파라미터 타입과 포인트컷에서 지정한 타입과 일치하는지 동적으로 검증을 한다.
  1. @target, @within

    • 둘 모두 지정한 어노테이션이 클래스에 붙어있는 조인 포인트를 찾는다.
    • 다만, @target의 경우 자식 클래스의 메서드와 부모의 메서드까지 어드바이스를 적용하는 반면,
      @within은 자식 클래스의 메서드에만 어드바이스를 적용한다.
    • 두 어노테이션 모두 클래스 단위로 확인한다.
  2. @annotation

    • 지정한 어노테이션을 지니고 있는 메서드와 매칭한다.
    • 메서드 단위로 적용 여부를 확인한다.
    • 어노테이션이 가지고 있는 값을 꺼낼 수 있다.
  3. @args

    • 런타임에 넘어온 파라미터 타입이 지정한 어노테이션을 지니고 있을 경우 매칭
  4. bean

    • 스프링에서만 제공하는 pcd로 빈 이름으로 매칭
  5. this

    • 프록시를 대상으로 하는 조인 포인트
    • 부모 타입 지원
  6. target

    • 타겟을 대상으로 하는 조인포인트
    • 부모 타입 지원

포인트컷 지시자 중 this, target, @within, @target, @annotation, @args 등은 어드바이스에서 매개변수를 전달받을 수 있다.
매개변수를 꺼내는 방법은 아래와 같다.

  1. JoinPoint에서 .getArgs() 이용
    • 다만 배열 형태로 반환이 된다.
  2. 포인트컷과 파라미터의 이름을 일치 시키기
    • 파라미터를 그대로 이용하면 됨
    • 이때 포인트컷의 타입은 파라미터로 지정한 타입으로 제한된다.(아래의 코드에서는 ClassAop로 제한)
@target(annotation)
public void test(ClassAop annotation)

프록시 생성 방식에 따라서 this에서 예상과 다른 결과가 나올 수 있다.
예를들어 Service(인터페이스)와 ServiceImpl이 있다고 했을 때,
JDK 동적 프록시는 Service를 구현한 프록시를 생성할 것이고,
CGLIB의 경우 인터페이스 여부 상관없이 구현 클래스를 활용해 프록시를 생성하므로 ServiceImpl을 상속받아 프록시를 생성한다.

만약 this(Service)를 포인트컷으로 지정했다면,

  • JDK 동적 프록시로 생성한 프록시와 CGLIB로 생성한 프록시 모두 Service를 부모로 가지고 있으므로 포인트컷은 True를 반환한다.

반면에 this(ServiceImpl)을 포인트컷으로 지정했다면,

  • JDK 동적 프록시의 경우 Service를 구현한 구체클래스, ServiceImpl도 Service를 구현한 구체클래스로 동일한 부모를 가지고 있지만, 프록시 객체와 ServiceImpl 두 객체 간에는 부모 자식 관계가 성립되지 않으므로 포인트컷은 false를 반환한다.
  • CGLIB의 경우 프록시가 ServiceImpl을 상속받아 생성되기 때문에 포인트컷이 true를 반환한다.

츨처 : 김영한 - 스프링 핵심 원리 고급편

profile
꾸준히 하자!

0개의 댓글