Spring AOP : 포인트컷 지시자

    이 포스팅은 인프런의 김영한님 강의를 듣고 복습하며 작성한 글입니다.

     


    포인트 컷 지시자

    앞선 글에서 스프링 AOP가 지원하는 @Aspect의 어드바이저 종류에 대해서 알아봤다. 이번 포스팅에서는 포인트컷 지시자에 대해서 정리하고자 한다. 

    포인트컷은 AnnotationAwareAutoProxyCreator가 @Aspect 어노테이션을 읽고 어드바이저를 가지는 프록시를 만들려고 할 때, 이 스프링 빈에 적용해도 되는지를 판단하는 기준이 된다. 포인트컷 지시자는 이 포인트컷이 어디에 적용될지를 정의할 수 있도록 도와주는 도구다. 

     


    포인트 컷 지시자 종류

    • execution : 가장 많이 씀. (접근지시자? / 반환타입 / 선언타입? / 메서드명 / 파라미터) 형태를 가짐.
      • ?는 생략 가능
      • 선언타입 : 부모타입 선언 → 자식 타입 적용 가능
      • 파라미터 : 부모타입 선언 불가능.
      • * : 어떤 패턴에도 매칭되는 것을 의미
      • . : 정확히 해당 패키지만 지칭
      • .. : 값이 정해져있지 않음. 어떤 패턴에도 매칭됨. 해당 패키지 + 하위 패키지 포함.
    • within : 선언타입을 대상으로 포인트컷 판단(exeuction에서 선언타입만 가져옴)
      • 부모 타입 선언 → 자식 타입 적용 불가능
    • bean : 빈 이름으로 포인트컷 적용 대상 판단
    • args : 변수 타입, 갯수로 포인트컷 적용 대상 판단
      • 부모 타입 선언 가능(String을 Object로 받을 수 있음)
      • 파라미터 전달 가능
      • 단독 사용 자제
    • @annotation : 특정 어노테이션이 달린 메서드면 포인트컷 적용 대상 판단
      • 파라미터 전달 가능
      • 파라미터 바인딩에 주로 사용
    • @within : 특정 어노테이션이 달린 클래스면 포인트컷 적용 대상 판단. 
      • 해당 객체가 자식 클래스 인 경우, 자식 클래스의 메서드만 포인트컷 적용 대상이 됨.
      • 파라미터 전달 가능
      • 파라미터 바인딩에 주로 사용
    • @target :  특정 어노테이션이 달린 클래스면 포인트컷 적용 대상 판단. 
      • 파라미터 전달 가능
      • 파라미터 바인딩에 주로 사용
      • 해당 객체가 자식 클래스 인 경우, 부모 클래스의 메서드도 포인트컷 적용 대상이 됨.
    • target : Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)이 조인 포인트
      • 파라미터 전달 가능
      • 파라미터 바인딩에 주로 사용
      • 프록시 내부의 'target'을 기준으로 판단함.
      • 프록시 내부의 'target'이 target 지시자 내부에 선언된 속성을 가질 수 있는지로 판단
    • this : 스프링 빈 객체(스프링 AOP 프록시) 대상이 조인 포인트
      • 파라미터 전달 가능
      • 파라미터 바인딩에 주로 사용
      • 프록시 객체를 기준으로 판단함.
      • 프록시 객체가 this 지시자 내부에 선언된 속성을 가질 수 있는지로 판단

     

    포인트 컷 지시자의 종류와 간단한 기능은 위에서 정리를 했다.

     


    포인트컷 지시자 : Execution

    기본 테스트 코드

    @Slf4j
    @SpringBootTest
    public class ExecutionTest {
    
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        Method helloMethod;
    
        @BeforeEach
        void init() throws NoSuchMethodException {
            helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
        }

     

    Signature 읽어오기

    @Test
    void printMethod() {
        //execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
        //생략 가능한 건, 접근 지시자, 패키지 + 클래스명임.
    
        //public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
        log.info("helloMethod = {}", helloMethod);
    }
    • helloMethod를 읽어온다. 결과 : "java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)" 
    • exeuction은 위 시그니쳐를 그대로 표현해서 포인트컷을 가져옴. 

     

    가장 정확한 execution 매치

    @Test
    void exactMatch() {
        pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • 접근 지시자, 반환 타입, 패키지, 클래스, 메서드, 변수 타입까지 다 명시함

     

    가장 많이 생략한 execution 매치

    @Test
    void allMatch() {
        pointcut.setExpression("execution(* *(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • 필수적인 부분(반환 타입 / 메서드명 / 변수 타입)만 *로 표시해서 맵핑했다.

     

    메서드 이름 맵핑

    @Test
    void nameMatch() {
        pointcut.setExpression("execution(* hello(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • 생략 가능한 부분을 다 생략하고, 메서드 이름만 명확하게 표시해서 맵핑했다.

     

    메서드 이름 맵핑 with *

    @Test
    void nameMatchStar1() {
        pointcut.setExpression("execution(* *lo(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    
    @Test
    void nameMatchStar2() {
        pointcut.setExpression("execution(* *l*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • *을 사용해서 메서드 이름의 패턴과 맵핑될 수 있도록 했다. 

     

    메서드 이름 맵핑 실패

    @Test
    void nameMatchFalse() {
        pointcut.setExpression("execution(* nono(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    • 해당 메서드는 클래스에 없다. 따라서 맵핑 실패한다.

     

    패키지 정확한 맵핑

    @Test
    void packageExactMatch1() {
        pointcut.setExpression("execution(* hello.aop.member.*.*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • member 패키지 내부에 있는 클래스 + 메서드만 대상이 되도록 맵핑했다. 

     

    패키지 정확한 맵핑2

    @Test
    void packageExactMatch2() {
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • member 패키지 내부에 실제 구현체까지 지칭해주었다.

     

    패키지 맵핑 실패 → 서브 패키지와 구분하기

    @Test
    void packageExactMatchFalse() {
        pointcut.setExpression("execution(* hello.aop.*.*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    • aop.*.*로 맵핑했다. 이 의미는 aop 패키지 안에 있는 클래스 + 메서드와 맵핑하겠다는 의미다.
    • 실제 대상은 aop.member에 있기 때문에 서브 패키지를 포함하지 않는 aop.*.*은 맵핑되지 않는다. 

     

    서브 패키지 맵핑

    @Test
    void packageExactSubPackage1() {
        pointcut.setExpression("execution(* hello.aop..*.*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • ".."을 이용해서 aop 하위에 있는 서브 패키지들도 맵핑 대상이 되도록 했다. 

     

    서브 패키지 맵핑

    @Test
    void packageExactSubPackage2() {
        pointcut.setExpression("execution(* hello.aop.member..*.*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    
    • ".."을 이용해서 aop.member 하위에 있는 서브 패키지들도 맵핑 대상이 되도록 했다.

     

    타입 매칭, 자식 타입 허용

    @Test
    void typeExactMatch() {
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    
    }
    • MemberService 인터페이스의 구현체인 MemberServiceImpl로 찍었다. 이것은 당연히 통과한다.

     

    타입 매칭, 부모 타입 허용 (중요)

    @Test
    void typeMatchSuperType() {
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • MemberServiceImpl의 인터페이스인 MemberService를 포인트컷 대상으로 설정했다.
    • 부모는 자식을 품을 수 있기 때문에 MemberService의 자식인 MemberServiceImpl은 포인트컷 적용 대상이 됨. 

     

    타입 매칭, 본인 메서드는 다 맵핑 대상이 됨(Internal)

    @Test
    void typeMatchInternal() throws NoSuchMethodException {
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(*))");
        Method internal = MemberServiceImpl.class.getMethod("internal", String.class);
        assertThat(pointcut.matches(internal, MemberServiceImpl.class)).isTrue();
    
    }
    • MemberServiceImpl은 MemberService를 구현했고, 별개로 internal() 메서드를 가진다.
    • 이 Internal() 메서드는 당연히 포인트컷 맵핑 대상이 된다. 

     

    타입 매칭, 부모 타입 매칭 시, 부모 타입에 있는 메서드만 포인트컷 대상이 됨(중요)

    @Test
    void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(*))");
        Method internal = MemberServiceImpl.class.getMethod("internal", String.class);
        assertThat(pointcut.matches(internal, MemberServiceImpl.class)).isFalse();
    }
    
    • MemberService로 맵핑을 하면, MemberServiceImpl은 포인트컷 대상이 되긴 된다. 부모는 자식을 품을 수 있기 때문이다.
    • 그런데 부모는 자식이 어떤 메서드를 가진지 모른다. 따라서 부모로 맵핑을 했을 때, 자식이 가지는 메서드는 포인트컷 대상이 되지 않는다.

     

    파라미터 맵핑 : 정확히 맵핑

    @Test
    void argsMatch() {
        pointcut.setExpression("execution(* *(String))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    
    }
    • MemberServiceImpl의 파라미터 타입은 모두 String이다. 동일하게 String을 표기해서 정확히 맵핑했다.

     

    파라미터 맵핑 : 공백 맵핑

    @Test
    void argsMatchNoArgs() {
        pointcut.setExpression("execution(* *())");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    
    • MemberServiceImpl의 파라미터 타입은 모두 String이다. 파라미터가 없는 메서드는 없기 때문에 위 포인트컷 대상은 없다.

     

    파라미터 맵핑 : Star 맵핑

    @Test
    void argsMatchStar() {
        pointcut.setExpression("execution(* *(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • Star는 어떤 문자가 와도 괜찮다는 의미다. 따라서 String이 올 수 있기 때문에, 포인트컷 대상이 된다.

     

    파라미터 맵핑 : .. 맵핑

    @Test
    void argsMatchAll() {
        pointcut.setExpression("execution(* *(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • ".."은 갯수가 몇개든 상관 없고, 어떤 문자든 상관없다는 의미다. 따라서 String 1개만 오는 것도 허용한다.

     

    파라미터 맵핑 : 복잡한 맵핑

    @Test
    void argsMatchComplex() {
        pointcut.setExpression("execution(* *(String, ..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • 첫번째 오는 인자는 String, 뒷쪽에는 인자 어떤 타입이 몇개가 오든 상관없다. 따라서 String만 있는 메서드도 포인트컷 사용이 가능하다. 

     

    파라미터 맵핑 : 부모 타입 맵핑은 실패 (중요)

    @Test
    void argsMatchComplex() {
        pointcut.setExpression("execution(* *(Object, ..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    • exeuction에서 파라미터 맵핑 시, 부모 타입은 맵핑할 수 없다. 따라서 최상위 클래스 Object는 String을 받을 수 없게 된다. 

     


    포인트컷 지시자 : Within

    WithIn 패키지 내의 특정 클래스를 포인트컷 대상으로 지정하는 것이다. Execution에서 선언 타입만 떼왔다고 이해를 할 수 있다. 그리고 WithIn은 부모 클래스를 지정하면, 자식 클래스는 포인트컷 대상이 되지 않는다. Exeuction은 부모 클래스를 지정하면, 자식 클래스는 포인트컷의 대상이 된다. 

     

    기본 코드

    @Slf4j
    public class WithInTest {
    
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        Method helloMethod;
    
        @BeforeEach
        void init() throws NoSuchMethodException {
            helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
        }
    }

     

    WithIn 정확한 맵핑

    @Test
    void withinExact() {
        pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    
    }
    • 패키지부터 클래스까지 정확하게 지정해주었다. 

     

    WithIn 맵핑 시, *로 패턴 맵핑 가능

    @Test
    void withinStar() {
        pointcut.setExpression("within(hello.aop.member.MemberService*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    • MemberServiceImpl을 MemberService*로 맵핑한 것이다. *는 앞서 이야기한 것처럼 어떤 패턴이 와도 상관없다.

     

    WithIn 맵핑, 패키지는 생략 가능. 클래스명은 반드시 필요(중요)

    //성공
    @Test
    void withinSubPackage() {
        pointcut.setExpression("within(hello.aop..MemberServiceImpl)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    
    
    //실패
    @Test
    void withinSubPackage() {
        pointcut.setExpression("within(hello.aop..)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    • withIn 맵핑은 선언 타입을 맵핑하는 것이기 때문에, 맵핑될 타입은 반드시 알려줘야한다. 
    • 패키지명은 축약이 가능하다. 

     

    WithIn 맵핑, 부모와 자식은 결별(중요)

    @Test
    void withinSuperTypeFalse() {
        pointcut.setExpression("within(hello.aop.member.MemberService)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    • MemberService(인터페이스)를 포인트컷 대상자로 지정했다.
    • withIn은 자식 클래스는 대상이 되지 않기 때문에 MemberServiceImpl은 포인트컷 맵핑이 되지 않는다.

     


    포인트컷 지시자 : args

    args는 메서드의 파라미터를 포인트컷 대상으로 설정한다. 이 때, execution은 부모 타입의 파라미터를 받을 수 없었다. 그렇지만 args는 부모 타입의 파라미터르 받을 수 있다. 또한 args는 파라미터 전달이 가능하다.

    args를 Rough하게 사용할 경우, 모든 스프링 빈에 프록시 객체가 적용이 될 수 있다. ".."로 설정해둘 경우 어떤 타입, 어떤 갯수가 와도 포인트컷 대상이 되기 때문이다. 이 때 문제가 되는 것은 final로 선언된 클래스들이 프록시 객체로 만들어 질 경우 오류가 발생할 수 있다는 점이다. 따라서 args는 단독으로 쓰기 보다는 다른 포인트컷 지시자로 포인트컷 대상을 줄인 다음에 사용된다. 

     

    기본 테스트 코드 

    @Slf4j
    public class ArgsTest {
    
        Method helloMethod;
    
        @BeforeEach
        public void init() throws NoSuchMethodException {
            helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
        }
    
        private AspectJExpressionPointcut pointcut(String expression) {
            AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
            pointcut.setExpression(expression);
            return pointcut;
        }
     }

     

    args 맵핑 

    @Test
    void 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();
    
    
        assertThat(pointcut("args(*)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
    
        assertThat(pointcut("args(*, ..)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
    
        assertThat(pointcut("args(..)")
                .matches(helloMethod, MemberServiceImpl.class)).isTrue();
    
        assertThat(pointcut("args(*,*)")
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
    
        assertThat(pointcut("args()")
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    • args는 부모 타입으로 자식을 받을 수 있다.
    • args는 *, ".." 등을 이용할 수 있다.

     

    args vs execution

    @Test
    void argsVsExecution() {
    
        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();
    
    
        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는 Object, Seriallizable 등의 부모 타입으로 String을 받을 수 있다.
    • execution은 Object, Seriallizable 등의 부모 타입으로 String을 받을 수 없다. 

     

    args 파라미터 바인딩 

    @Around("allMember() && args(arg, ..)")
    public Object logArgs2(ProceedingJoinPoint joinPoint, Object arg) throws Throwable {
        log.info("[logArgs2]{}, arg = {} ", joinPoint.getSignature(), arg);
        return joinPoint.proceed();
    }
    1. 어드바이스에 arg를 선언하고, 받을 객체의 타입을 선언한다.
    2. 어드바이스 파라미터의 변수명과 args 내부의 변수명을 동일하게 해준다.
    3. 이 때, 어드바이스 arg는 이미 어떤 객체 타입인지 알고 있다. 포인트컷에는 이 객체 정보가 arg라는 이름으로 넘어가있기 때문에 맵핑이 된다.

     


    포인트컷 지시자 : bean

    bean으로 지정된 이름을 가진 스프링 빈이 포인트컷 적용 대상이 된다.

     

    bean 포인트컷 적용

    @Slf4j
    @Aspect
    static class BeanAspect{
        @Around("bean (orderService) || bean (*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    • bean 이름이 orderService, *Repository인 경우 포인트컷 적용 대상이 된다.
    • 이 어드바이저를 스프링 빈 등록하면, @Aspect를 보고 orderService, *Repository 이름을 가진 스프링 빈에 AOP 프록시가 적용된다.

     


    포인트컷 지시자 : @annotation

    @annotation에서 지정한 어노테이션을 가지고 있는 '메서드'가 포인트컷 대상이 된다. 

     

    @annotation 적용

    @Slf4j
    @Aspect
    static class AtAnnotationAspect{
        // 아래 패키지에 만들어둔 @MethodAop에 걸리는 애들이 있으면 그걸 AOP로 만들겠다는 뜻임.
        @Around("@annotation(hello.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    
    }
    • hello.aop.member.annotation 패키지에는 MethodAop라는 어노테이션이 있다.
    • @annotation으로 해당 어노테이션을 지정했다. 
    • 따라서 @MethodAop public void String hello(String hello) 같이 메서드 위에 @MethodAop 어노테이션이 붙은 메서드는 포인트컷 대상이 된다. 

     


    포인트컷 지시자 : @target, @within 

    • @target, @within은 모드 특정 어노테이션이 붙은 클래스를 포인트컷으로 판단하는 포인트컷 지시자다. 
    • @target은 자식 클래스가 포인트컷 대상이 되었을 때, 부모 클래스에 있는 메서드까지 포인트컷 적용 대상이 된다.
    • @within은 자식 클래스가 포인트컷 대상이 되었을 때, 오로지 자식 클래스에 있는 메서드만 포인트컷 적용 대상이 된다. 

     

    @within, @target 기본 코드

    @Slf4j
    @SpringBootTest
    @Import(AtTargetWithinTest.Config.class)
    public class AtTargetWithinTest {
    
    
        @Autowired
        Child child;
    
        @Test
        void success() {
            log.info("child Proxy = {}", child.getClass());
            
            // @within, @target 모두 동작
            child.childMethod();
            
            // parentMethod는 자식에서 오버라이드 되지않음 → 부모에만 있음
            // @within 적용 X, @target 적용 O
            child.parentMethod();
        }
    
        static class Config {
    
            @Bean
            public Parent parent() {
                return new Parent();
            }
    
            @Bean
            public Child child() {
                return new Child();
            }
    
            @Bean
            public AtTargetWithinAspect atTargetWithinAspect() {
                return new AtTargetWithinAspect();
            }
    
        }
    
    
        static class Parent{
            public void parentMethod(){}
        }
    
        @ClassAop
        static class Child extends Parent{
            public void childMethod(){}
        }
    
    
    
    
    
    }

     

    @within, @target 포인트컷 작성 코드.

    @Aspect
    static class AtTargetWithinAspect{
    
        @Pointcut("execution(* hello.aop..*(..))")
        public void allOrder(){}
    
        @Around("allOrder() && @within(hello.aop.member.annotation.ClassAop)")
        public Object atTargetAspect1(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@within log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    
        @Around("allOrder() && @target(hello.aop.member.annotation.ClassAop)")
        public Object atTargetAspect2(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@target log] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
    • hello.aop.member.annotation 패키지에 있는 ClassAop라는 어노테이션을 가지고 있는 클래스가 적용 대상이 됨. 

     

    실행 결과

    • @within 대상이 자식 클래스면, 부모 클래스의 메서드에는 적용이 안된다. → 1개만 적용
    • @target 대상이 자식 클래스면, 부모 클래스의 메서드에도 적용이 된다. → 부모, 자식 둘다 적용

     

     


    파라메터 전달

    특정 포인트컷 지시자들은 어드바이스에서 선언한 파라미터를 받을 수 있다. 이런 구성의 특징은 어드바이스에서 선언한 파라미터에 이미 '타입'이 설정되어있다는 점이다. 포인트컷 지시자에서는 '타입'이 필요하니, 어드바이스에서 선언한 파라미터의 타입만 넘겨주면 된다. 

    타입을 넘기는 방법은 어드바이스에서 선언한 파라미터의 이름과 포인트컷 지시자에서의 파라미터 이름을 맞춰주면 된다. 그러면 파라미터의 이름으로 맵핑이 되어, 파라미터의 클래스가 포인트컷으로 넘어가게 된다.

    예를 들어 다음과 같이 된다. String arg1라고 선언되어있을 때, 변수를 넘겨주면 args( arg1 ) → args ( String) 치환되는 것으로 이해하면 좀 더 알기 쉽다.

     

    테스트 코드

    @Slf4j
    @SpringBootTest
    @Import(ParameterTest.ParameterAspect.class)
    public class ParameterTest {
    
    
        @Autowired
        MemberService memberService;
    
        @Test
        void success() {
            log.info("memberService Proxy = {}", memberService.getClass());
            memberService.hello("helloA");
        }

     

    파라미터 넘기는 코드 작성

    @Aspect
    static class ParameterAspect {
    
        @Pointcut("execution(* hello.aop.member..*.*(..))")
        private void allMember() {}
    
    
        @Before("allMember()")
        public void doLog1(JoinPoint joinPoint) {
            Object arg = joinPoint.getArgs()[0];
            log.info("doLog 1, args : {}, joinPoint : {}", arg, joinPoint.getSignature());
        }
    
        @Before("allMember() && args(arg)")
        public void doLog2(JoinPoint joinPoint, String arg) {
            log.info("doLog 2, args : {}, joinPoint : {}", arg, joinPoint.getSignature());
        }
    
    
        @Before("allMember() && args(arg)")
        public void doLog3(JoinPoint joinPoint, Object arg) {
            log.info("doLog 3, args : {}, joinPoint : {}", arg, joinPoint.getSignature());
        }
    
        @Before("allMember() && @annotation(annotation)")
        public void doLog4(JoinPoint joinPoint, MethodAop annotation) {
            log.info("doLog 4, annotation : {}, joinPoint : {}", annotation, joinPoint.getSignature());
        }
    
    
        @Before("allMember() && @within(annotation)")
        public void doLog5(JoinPoint joinPoint, ClassAop annotation) {
            log.info("doLog 5, annotation : {}, joinPoint : {}", annotation, joinPoint.getSignature());
        }
    
    
        @Before("allMember() && @target(annotation)")
        public void doLog6(JoinPoint joinPoint, ClassAop annotation) {
            log.info("doLog 5, annotation : {}, joinPoint : {}", annotation, joinPoint.getSignature());
        }
    
    
        @Before("allMember() && this(obj)")
        public void doLog7(JoinPoint joinPoint, MemberService obj) {
            log.info("doLog 7, this obj : {}, joinPoint : {}", obj.getClass(), joinPoint.getSignature());
        }
    
    
        @Before("allMember() && target(obj)")
        public void doLog6(JoinPoint joinPoint, MemberService obj) {
            log.info("doLog 8, this obj : {}, joinPoint : {}", obj.getClass(), joinPoint.getSignature());
        }
    
    
    }
    • joinpoint 인스턴스는 내부적으로 메서드의 파라미터를 가지고 있고, getArgs()로 가져올 수 있다.
    • args, @annotation, @within, @target, this, target을 파라미터로 넘길 수 있다. 동작 메커니즘은 위와 동일하다. 파라미터에 이미 클래스가 선언되어있으니, 그걸 이름으로 맵핑한다는 개념이다. 

     


    포인트컷 지시자 : this, target

    this, target에 대한 기초

    먼저 this, target은 *, ".."을 이용해서 Rough하게 표현할 수 없다. 정확하게 하나를 가리켜줘야한다. 

     

    target은 스프링 AOP의 대상이 되는 'target'을 대상으로 포인트컷이 적용되는지를 본다. 예를 들어 이걸 스프링 프록시로 만들었을 때, 타겟이 target(..)안에 선언된 클래스와 매칭이 될 수 있을지를 보고 포인트컷 적용 대상을 판단한다.

    this는 스프링 컨테이너에 등록된 스프링 AOP 프록시 빈을 대상으로 살펴본다. 이 AOP 프록시 빈은 내부적으로 Target을 가지고 있는 객체다. 앞에서 계속 이야기하던 객체다. 

     

    this, target에 대한 심화

    this, target은 각각 스프링 빈 AOP과 스프링 빈 AOP의 객체를 가리킨다. 이런 차이 때문에 실제 AOP 적용을 할 때 차이가 발생한다. 왜냐하면 스프링이 빈 객체를 생성하는 방법이 JDK 동적 프록시, CGLIB 동적 프록시 두 가지가 있기 때문이다.

    앞의 내용을 살짝 복습해보자. 프록시 팩토리는 인터페이스가 있는 경우, 인터페이스를 구현한 JDK 동적 프록시를 만들어준다. 분리해서 보면 JDK 동적 프록시는 인터페이스를 구현한 프록시 객체고, 실제 타겟은 인터페이스를 구현한 구현체다.

    반대로 프록시 팩토리는 인터페이스가 없는 경우,  CGLIB 라이브러리를 이용해 구현체를 상속받은 프록시 객체를 만들어주고, 내부적으로 이 프록시 객체는 실제 구현체를 가진다. 분리해서 보면 CGLIB 동적 프록시는 구현체를 상속받은 객체, 타렛은 구현체 그 자체다.

    이처럼 JDK 동적 프록시와 CGLIB 동적 프록시는 차이를 가진다. 결론은 프록시 객체가 인터페이스를 구현했느냐, 실제 객체를 상속받았느냐의 차이다. 이 차이가 this, target의 포인트컷 적용에 있어서 차이를 가져온다. 

     

    This는 프록시 객체를 바라본다. 프록시 객체가 특정 타입에 맵핑이 되는지를 본다. JDK 동적 프록시와 CGLIB 동적 프록시를 나눠서보자.

    JDK 동적 프록시 객체에 This가 적용되었다고 해보자. 이 때, this에 인터페이스가 선언되었다. JDK 동적 프록시는 이 인터페이스를 구현했기 때문에 이 인터페이스를 알고 있다. 따라서 포인트컷 적용 대상이 된다.

    반대로 JDK 동적 프록시 객체, this에는 구현체가 선언되었다고 해보자. JDK 동적 프록시는 인터페이스를 구현한 또 다른 구현체이기 때문에 실제 구현체는 어떤지 알지 못한다. 따라서, JDK 동적 프록시 구현체 타입과 실제 타겟 구현체 타입은 서로 관련이 없다. 따라서 이 경우 포인트컷 적용 대상이 되지 못한다.

    CGLIB 동적 프록시 객체에 This가 적용되었다고 해보자. 이 때, this에 인터페이스가 선언되었다. CGLIB 동적 프록시 객체는 구현체를 상속받은 또 다른 구현체다. 따라서 이 프록시 객체는 부모 클래스의 인터페이스를 알고 있다. 따라서 AOP 적용이 가능하다.

    CGLIB 동적 프록시 객체에 this에 구현체가 선언되었다고 하자. CGLIB 동적 프록시 객체는 구현체를 상속받은 또 다른 구현체다. 따라서 구현체를 알고 있기 때문에 AOP 적용이 가능하다. 

     

    target는 실제 타겟을 바라본다. 프록시 내부의 타겟 객체가 특정 타입에 맵핑이 되는지를 본다. JDK 동적 프록시와 CGLIB 동적 프록시를 나눠서보자.

    JDK 동적 프록시 객체에 target이 적용되었다고 해보자. 이 때, target에 인터페이스가 선언되었다. JDK 동적 프록시의 타겟은 인터페이스를 구현한 구현체이기 때문에 인터페이스를 알고 있다. 따라서 포인트컷 적용 대상이 된다.

    JDK 동적 프록시 객체에 target이 적용되었고, target에는 구현체가 선언되었다고 해보자. JDK 동적 프록시 내부의 타겟은 인터페이스를 구현한 구현체 그 자체다. 따라서 포인트컷 적용 대상이 된다.

    CGLIB 동적 프록시 객체에 target이 적용되었다고 해보자. 이 때, target에 인터페이스가 선언되었다. CGLIB 동적 프록시의 target 객체는 인터페이스를 구현한 구현체다. 따라서 포인트컷 적용 대상이 된다.

    CGLIB 동적 프록시 객체에 target에 구현체가 선언되었다고 하자. CGLIB 동적 프록시 객체의 내부 target은 구현체 그 자체다. 따라서 포인트컷 적용 대상이 된다. 

     

    정리하면 target은 CGLIB, JDK 동적 프록시 상관없이 항상 동일하게 AOP가 적용이 된다. 하지만 this는 JDK 동적 프록시로 만들어졌을 경우, JDK 동적 프록시 객체 자체가 실제 구현체가 어떤지 알지 못하기 때문에 AOP 적용 대상이 되지 않는다는 것이다. 

     

    테스트 코드

    @Slf4j
    @Import(ThisTargetTest.AspectClass.class)
    //@SpringBootTest
    @SpringBootTest(properties = "spring.aop.proxy-target-class=false") // JDK 동적 프록시
    public class ThisTargetTest {
    
        @Autowired
        MemberService memberService;
    
        @Test
        void test1() {
            log.info("memberService Proxy = {}", this.memberService.getClass());
            this.memberService.hello("helloA");
        }
    }

     

    포인트컷 지시자 코드

    @Aspect
    static class AspectClass{
    
        @Around("this(hello.aop.member.MemberService)")
        public Object this1(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("this-interface : {}", joinPoint.getSignature() );
            return joinPoint.proceed();
        }
    
        @Around("target(hello.aop.member.MemberService)")
        public Object target1(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("target-interface : {}", joinPoint.getSignature() );
            return joinPoint.proceed();
        }
    
        @Around("this(hello.aop.member.MemberServiceImpl)")
        public Object this2(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("this-concrete : {}", joinPoint.getSignature() );
            return joinPoint.proceed();
        }
    
        @Around("target(hello.aop.member.MemberServiceImpl)")
        public Object target2(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("target-Concrete : {}", joinPoint.getSignature() );
            return joinPoint.proceed();
        }
    }
    • JDK 동적 프록시인 경우, this + concrete(impl)은 AOP적용 되지 않음.

     

    실행 결과

    • JDK 동적 프록시인 경우, this + concrete(impl)은 AOP적용 되지 않는 것을 확인할 수 있다.

    댓글

    Designed by JB FACTORY