Spring

[Spring] 포인트컷 지시자 (AspectJ Pointcut Expression) - 3

이덩우 2024. 5. 11. 01:36

포인트컷 지시자에 관한 마지막 포스팅이다.

이번 포스팅에서는 커스텀 애노테이션을 생성해 포인트컷 표현식으로 활용할 수 있는 방법과 어드바이스에 다양한 매개변수를 전달해 활용하는 방법에 대해 알아볼 것이다.

 

1. 예제 세팅

먼저 커스텀 애노테이션을 생성하자.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
    String value();
}

 

`ClassAop`의 경우 클래스 레벨에 붙일 애노테이션으로, @Target의 값을 `ElementType.TYPE`으로 지정했다.

또한 리플렉션을 통해 애노테이션 정보를 확인할 것이므로 런타임 시점까지 애노테이션이 살아있어야한다. @Retention의 값을 `RetentionPolicy.RUNTIME`으로 설정한다.

 

기존에 테스트하던 MemberServiceImpl에 애노테이션을 추가해보자.

@ClassAop
@Component
public class MemberServiceImpl implements MemberService{
    @Override
    @MethodAop(value = "test value")
    public String hello(String param) {
        return "ok";
    }

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

 


2. 애노테이션을 활용한 포인트컷 표현식

- 클래스 레벨에 붙는 애노테이션 매치, @target, @within

`@target`지시자와 `@within`지시자는 모두 클래스 레벨에 붙은 애노테이션을 매치하는 포인트컷 지시자이다.

그럼 무슨 차이가 있을까?

 

`@target` 지시자는 인스턴스 레벨, 실행 객체의 모든 메서드를 조인 포인트로 적용한다. 따라서 부모 타입이 존재한다면, *부모 타입의 메서드에도 AOP를 적용한다.*

반면 `@within` 지시자는 *정확히 주어진 타입에 존재하는 메서드에 대해서만 조인 포인트로 적용한다.* within이라는 말 자체가 안쪽에 있는 것을 한정 짓는다는 의미로 받아들이면 이해가 편하다.

 

예제 테스트 코드를 통해 살펴보자.

@SpringBootTest
@Slf4j
public class AtTargetWithinTest {

    @Autowired
    Child child;

    @TestConfiguration
    static class Config {
        @Bean
        public Parent parent() {
            return new Parent();
        }

        @Bean
        public Child child() {
            return new Child();
        }

        @Bean
        public AtTargetAtWithinAspect atTargetAtWithinAspect() {
            return new AtTargetAtWithinAspect();
        }
    }

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

    static class Parent {
        public void parentMethod() {
        } // 부모에만 있는 메서드
    }

    @ClassAop
    static class Child extends Parent {
        public void childMethod() {
        }
    }

    @Test
    void success() {
        log.info("child proxy={}", child.getClass());
        child.childMethod();
        child.parentMethod();
    }

}

테스트 결과

테스트 결과 예상대로 @within을 사용한 경우 부모 메서드에는 AOP가 적용되지 않았다.

 

- args, @args, @target의 주의사항

위 포인트컷 지시자는 단독으로 사용하면 오류가 발생할 수 있다.

직전 예제에서도 `execution()` 지시자를 통해 범위를 좁히고 `@target` 지시자를 사용한 모습을 볼 수 있는데, 이유는 아래와 같다.

 

`args`, `@args`, `@target`의 경우 *실제 객체 인스턴스가 생성된 뒤 포인트컷 적용 여부를 확인할 수 있다.*

이 말은 결국 실행 시점에 우선 프록시가 생성되어 있어야하고, 스프링 빈으로 등록되어있어야한다는 것을 의미한다.

(프록시가 생성되어 있어야 포인트컷 적용 여부를 확인하든 말든 하니까)

문제는 프록시가 스프링 컨테이너가 생성되는 애플리케이션 로딩 시점에 스프링 빈으로 등록되는데, 현재로써는 어디에 프록시를 만들어야할 지 모르니 *기본적으로 등록되는 스프링 빈을 포함한 모든 빈을 프록시 객체로 등록하려한다.*

기본적으로 등록되는 스프링 빈에는 *final로 지정된 빈*들도 있기 때문에 오류가 발생할 수 있다.

 

따라서 단독으로 사용하는게 아니라 범위를 좁힌 뒤 AND 연산자를 통해 같이 사용하도록 하자.

 

- 메서드 레벨에 붙는 애노테이션 매치, @annotation

`@annotation`지시자는 메서드 레벨에 붙는 애노테이션을 매치 해준다.

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

    @Autowired
    MemberService memberService;

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

테스트 결과

정상적으로 메서드 레벨에 `@MethodAop` 애노테이션이 붙어있는 `hello()`메서드에만 AOP가 적용되었다.

 

@Transactional 애노테이션의 경우도 클래스 레벨 혹은 메서드 레벨에 붙이면 public 메서드에 대해 트랜잭션 AOP가 적용된다. 
이 역시 스프링은 내부적으로 @target 혹은 @within, @annotation을 사용할까? 찾아봐야겠다.

 


3. 어드바이스에 매개변수 전달

포인트컷 지시자를 조금 특별하게 사용할 수 있다. 어드바이스의 매개변수로 JoinPoint외에 다양한 객체를 받아 활용이 가능하다.

@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 {
        /**
         * 얘내는 파라미터 바인딩을 목적으로 하는 것.
         * 포인트컷 표현식으로써 필터링하는 것과 쓰임새가 다름
         * -> 타입을 넣냐, 파라미터 명을 넣냐 차이
         */
        @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 atTargetArgs(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@target]{}, obj={}", joinPoint.getSignature(), annotation);
        }

        @Before("allMember() && @within(annotation)")
        public void atWithinArgs(JoinPoint joinPoint, ClassAop annotation) {
            log.info("[@within]{}, obj={}", joinPoint.getSignature(), annotation);
        }

        @Before("allMember() && @annotation(annotation)")
        public void atAnnotationArgs(JoinPoint joinPoint, MethodAop annotation) {
            log.info("[@annotation]{}, annotationValue={}", joinPoint.getSignature(), annotation.value());
        }
    }
}

 

@Before 지시자를 사용하는 부분부터 보면 편하다.

- args

기존 `args`지시자는 내부에 메서드 매개변수의 타입을 지정하는 역할을 했다.

하지만 임의의 변수명을 넣고, 동일한 변수명으로 어드바이스의 매개변수로 받을 수 있다.

물론 매개변수 타입은 정확히 맞추거나 상위 부모 타입으로 맞춰야 사용이 가능하다.

 

- this

프록시 객체를 전달받는다. 

 

- target

실제 타깃을 전달받는다.

 

- @target, @within

클래스에 붙은 애노테이션을 전달받는다.

 

- @annotation

메서드에 붙은 애노테이션을 전달받는다.

 

 

 

출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편