[Spring] 스프링 AOP - Pointcut 표현식

AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공합니다.

스프링에서는 포인트컷 표현식에서 사용하기 위해 AspectJ가 제공하는 포인트컷 지시자(Pointcut Designator) 줄여서 PCD를 지원합니다.

 

포인트컷 지시자의 종류

포인트컷 지시자는 아래와 같이 여러개가 있지만, 주로 execution을 자주 사용합니다.

  • execution : 메소드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다.
  • within : 특정 타입 내의 조인 포인트를 매칭한다.
  • args : 인자가 주어진 타입의 인스턴스인 조인 포인트
  • this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target : Target 객체(스프링 AOP 프록시가 가르키는 실제 대상)를 대상으로 하는 조인 포인트
  • @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
  • @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
  • @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
  • @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
  • bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정한다

execution

메소드 실행 조인 포인트를 매칭합니다.

 

execution 문법을 예시로 보면 다음과 같습니다.

여기서 * 은 아무 값이 들어와도 된다는 뜻이고, 파라미터에서의 .. 은 파라미터의 타입과 파라미터 수가 상관없다는 뜻입니다.

 

위 예시 문법을 해석해보면 다음과 같습니다.

반환 타입 모두 허용, hello.aop.test 패키지에 있는 모든 클래스의 모든 메서드, 그리고 그 메서드의 파라미터 타입과 파라미터 수가 상관 없이 포인트 컷을 적용.

 

위 예시코드는 hello.aop.test 패키지와 일치하는 모든 것에 해당되고, 그 하위 패키지는 포인트컷의 대상이 되지 않습니다.

하지만 패키지의 하위 패키지까지 포인트 컷의 대상이 되도록 하고 싶다면 다음과 같이 패키지 경로 다음에 ..을 붙여주면 됩니다.

"execution(* hello.aop.test..*.*(..))" hello.aop.test 패키지와 그 하위에 있는 패키지 전부

 

부모타입을 포인트컷 대상으로 지정하는 경우

만약 부모타입을 포인트컷 대상으로 지정할 경우 자식 타입도 대상이 됩니다.

아래의 예시 코드는 MemberService 인터페이스를 포인트 컷 대상으로 지정하고, 이 포인트 컷이 MemberService의 구현체인 MemberServiceImpl과 같은지 확인하는 테스트 코드입니다.

@Test
void typeMatchSuperType() {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))"); // 부모타입을 선언해도 자식 타입은 매칭된다.
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

하지만, 부모타입을 지정할 경우 자식 타입에만 있는 메서드는 포인트 컷 대상이 되지 않습니다.

@Test
void typeMatchNoSuperTypeMethodFalse() throws NoSuchMethodException {
    pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))"); // 부모타입을 선언하면 자식 타입에만 있는 메서드는 매칭되지 않는다.
    Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);
    assertThat(pointcut.matches(internalMethod, MemberServiceImpl.class)).isFalse();
}

 

파라미터 매칭 규칙

execution의 파라미터 매칭 규칙은 다음과 같습니다.

  • (String) : 메서드의 파라미터가 정확하게 String 타입의 파라미터이어야  포인트컷 대상
  • () : 메서드의 파라미터가 없어야 포인트컷 대상
  • (*) : 메서드의 파라미터 타입은 모든 타입을 허용하지만, 정확히 하나의 파라미터를 가진 메서드가 포인트컷 대상
  • (*, *) : 메서드의 파라미터 타입은 모든 타입을 허용하지만, 정확히 두 개의 파라미터를 가진 메서드가 포인트컷 대상
  • (..) : 메서드의 파라미터 수와 무관하게 모든 파라미터, 모든 타입을 허용한다. ( 파라미터가 없어도 된다. )
  • (String, ..) : 메서드의 첫 번째 파라미터는 String 타입으로 시작해야 하고, 나머지 파라미터 수와 무관하게 모든 파라미터, 모든 타입을 허용한다. ( Ex:// (String) , (String, xxx) , (String, xxx, xxx) 허용 )

within

특정한 타입(클래스) 내의 조인 포인트에 대해 매칭합니다.

쉽게 이야기해서 execution에서 타입(클래스) 부분만 사용하는 것이라 할 수 잇습니다.

주의할점으로는, within은 타겟의 타입에만 직접 적용하기 때문에, 부모 타입을 지정해서는 안됩니다. (정확하게 타입이 맞아야 함)

 

public class WithinTest {
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;
    
    @BeforeEach // 각 테스트의 충돌을 방지 하기위해 사용
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }
    
    @Test
    @DisplayName("hello.aop.member 패키지에서 MemberServiceImpl 타입만 매칭")
    void withinExact() {
        pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    
    @Test
    @DisplayName("hello.aop.member 패키지에서 Service가 포함된 타입만 매칭.")
    void withinStar() {
        pointcut.setExpression("within(hello.aop.member.*Service*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
    
    @Test
    @DisplayName("hello.aop.member 패키지와 그 하위의 모든 타입 매칭)
    void withinSubPackage() {
        pointcut.setExpression("within(hello.aop..*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    @DisplayName("타켓의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
    void withinSuperTypeFalse() {
        pointcut.setExpression("within(hello.aop.member.MemberService)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
    @Test
    @DisplayName("execution은 타입 기반, 인터페이스를 선정 가능.")
    void executionSuperTypeTrue() {
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
}

 


args

인자가 주어진 타입의 인스턴스인 조인 포인트로 매칭합니다.

기본 문법은 execution의 파라미터 부분과 같으나, execution과 다르게 부모 타입을 허용합니다.

 

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;
    }
    
    /**
     * 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();
        assertThat(pointcut("execution(* *(java.io.Serializable))") //매칭 실패
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
        assertThat(pointcut("execution(* *(Object))") //매칭 실패
                .matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }
}

자바가 기본으로 제공하는 String 은 Object , java.io.Serializable 의 하위 타입으로 정적으로 클래스에 선언된 정보만 보고 판단하는 execution(* *(Object)) 는 매칭에 실패하지만,

동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는 args(Object) 는 매칭에 성공합니다. (부모 타입 허용)


@target, @within

  • @target : 타입의 애노테이션이 있는 클래스의 메서드와 그 클래스의 부모 클래스의 메서드까지 매칭)
  • @within : 주어진 애노테이션이 있는 타입 내에 있는 클래스의 메서드만 매칭
    @Slf4j
    @Aspect
    static class AtTargetAtWithinAspect {
        // @target: ClassAop 어노테이션이 붙은 메서드를 대상으로 선정, 부모 타입의 메서드도 적용
        @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] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
...생략... [@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()

...생략... [@within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()

쉽게 이야기해서 @target 은 부모 클래스의 메서드까지 어드바이스를 다 적용하고, @within 은 자기 자신의 클래스에 정의된 메서드에만 어드바이스를 적용 합니다.

 

주의할점으로 위 코드를 보면 execution으로 대상 범위를 축소하였습니다.

이 처럼 args, @args, @target 같은 표현식은 최대한 프록시 적용 대상을 축소하는 표현식과 함께 사용해야 오류가 발생하지 않습니다.

 


@annotation

@annotation : 주어진 애노테이션을 갖고 있는 메서드를 대상으로 매칭
@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();
    }
}

위 코드에서는 직접만든 @MethodAop 어노테이션이 붙은 메서드가 대상이되어 어드바이스가 적용됩니다.


매개변수 전달

다음은 포인트컷 표현식을 사용해서 어드바이스에 매개변수를 전달할 수 있습니다.

this, target, args,@target, @within, @annotation, @args

 

사용방법은 아래처럼 포인트컷의 이름과 매개변수의 이름을 맞추어야 합니다.

여기서는 arg로 맞추었으며, 타입이 메서드에 지정한 타입으로 제한됩니다.

여기서는 메서드의 타입을 String으로 지정하였기 때문에 args(arg,..)가 => args(String,..)으로 변환됩니다.

@Before("allMember() && args(arg,..)")
public void logArgs3(String arg) {
    log.info("[logArgs3] arg={}", arg);
}

 

아래의 예시 코드를 봅시다.

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

    @Autowired
    MemberService memberService;

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

    @Slf4j
    @Aspect
    static class ParameterAspect {
    
        @Before("allMember() && args(arg,..)")
        public void logArgs(String arg) {
            log.info("[logArgs3] arg = {} ", arg);
        }

        @Before("allMember() && this(obj)")
        public void thisArgs(JoinPoint joinPoint, MemberService obj) { // this : 프록시를 반환
            log.info("[this]{}, obj = {} ", joinPoint.getSignature(), obj.getClass());
        }

        @Before("allMember() && target(obj)")
        public void targetArgs(JoinPoint joinPoint, MemberService obj) { // target : 실제 구현체를 반환
            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]{}, annotation.value = {} ", joinPoint.getSignature(), annotation.value());
        }
    }
}

 

실행 결과를 보면 다음과 같습니다.

..생략..ParameterTest$ParameterAspect: [logArgs] arg = helloA 
..생략..ParameterTest$ParameterAspect: [this]String hello.aop.member.MemberServiceImpl.hello(String), obj = class hello.aop.member.MemberServiceImpl$$EnhancerBySpringCGLIB$$86b6a519 
..생략..ParameterTest$ParameterAspect: [target]String hello.aop.member.MemberServiceImpl.hello(String), obj = class hello.aop.member.MemberServiceImpl 
..생략..ParameterTest$ParameterAspect: [@target]String hello.aop.member.MemberServiceImpl.hello(String), obj = @hello.aop.member.annotation.ClassAop() 
..생략..ParameterTest$ParameterAspect: [@within]String hello.aop.member.MemberServiceImpl.hello(String), obj = @hello.aop.member.annotation.ClassAop() 
..생략..ParameterTest$ParameterAspect: [@annotation]String hello.aop.member.MemberServiceImpl.hello(String), annotation.value = test value

 

  • logArgs : 매개변수를 전달 받는다.
  • this : 프록시 객체를 전달 받는다.
  • target : 실제 대상 객체를 전달 받는다.
  • @target , @within : 타입의 애노테이션을 전달 받는다.
  • @annotation : 메서드의 애노테이션을 전달 받는다. (여기서 처럼annotation.value() 로 해당 애노테이션의 값을 출력할 수 도 있습니다.)


참고자료 : 김영한의 스프링 부트 핵심 원리 - 고급편