본문 바로가기

Spring Boot

Spring AOP - 포인트컷(pointcut) 지시자(@annotation, this, target)

728x90

해당 포스팅에 이어 @annotation, bean 지시자도 알아보겠습니다.

  • @annotation: 특정 애너테이션이 적용된 메소드에 대해 포인트컷을 설정합니다. 즉, 메서드에 특정 애너테이션이 붙어 있을 때 그 메서드를 가로챌 수 있습니다.

[참고]

 

  • @annotation: 메서드 수준의 애너테이션을 타겟으로 함.
  • @target: 클래스 수준의 애너테이션을 타겟으로 함.

 

@annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyCustomAnnotation {
}

public class MyService {
    @MyCustomAnnotation
    public void myAnnotatedMethod() {
        // some logic
    }
}

@Aspect
public class MyAspect {
    // @MyCustomAnnotation 어노테이션이 붙은 메소드에 어드바이스 적용
    @Pointcut("@annotation(com.example.annotation.MyCustomAnnotation)")
    public void annotatedMethod() {}

    @Before("annotatedMethod()")
    public void beforeAdvice() { // myAnnotatedMethod가 호출될 때 가로챌 수 있습니다. 
        // Advice logic
    }
}

 

 

참고 (@annotation vs execution)

 

@annotation 
- 장점: 조인포인트를 명확하게 표시할 수 있습니다. 따라서 해당 애노테이션을 보는 개발자는 해당 메서드나 클래스에 특정 Aspect가 적용되었음을 쉽게 알 수 있습니다.

- 단점: 특정 메서드나 클래스에 애노테이션을 추가해야 합니다. 이는 추가적인 작업이 될 수 있으며, 모든 조인포인트에 일관성 있게 애노테이션을 붙이는 것이 어려울 수도 있습니다.

execution 
- 장점: 조인포인트를 코드 내에서 직접 명시하지 않고 Aspect에서 패턴을 사용해 정의할 수 있습니다. 이로 인해 Aspect와 비즈니스 로직의 분리가 더욱 명확해집니다.

- 단점: 조인포인트가 어디에 적용되었는지 코드만 보고 즉시 파악하기가 어려울 수 있습니다. 따라서 Aspect의 로직을 이해하기 위해서는 해당 지시자의 패턴을 잘 파악해야 합니다.

그런 장단점 때문에 실무에서는 둘다 사용하게 되지만, 막상 사용해보면 애노테이션이 좀 더 직관적이고 실용적이기 때문에 많은 개발자들이 애노테이션 방식을 더 선호하는 경향이 있습니다.

아래는 추가 예제입니다.


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

@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 {
       // MethodApp 에너테이션이 걸린 MemberServiceImpl(MemberService)의 hello 메소드에 어드바이스 적용
        @Around("@annotation(hello.aop.member.annotation.MethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

 


매개변수 전달

지금까지 배운 this, target, args,@target, @within, @annotation 포인트컷들은 어드바이스에 매개변수를 전달할 수 있습니다.

 

아래와 같이 사용합니다. (arg 가 매개변수입니다.)

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

 

아래는 다양한 매개변수 전달 예시 확인을 위한 코드입니다.


@ClassAop
@Component
public class MemberServiceImpl implements MemberService {

    @Override
    @MethodAop("test value")
    public String hello(String param) {
        return "ok";
    }

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

package hello.aop.pointcut;

import hello.aop.member.MemberService;
import hello.aop.member.annotation.ClassAop;
import hello.aop.member.annotation.MethodAop;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

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

        @Around("allMember()")
        public Object logArgs1(ProceedingJoinPoint joinPoint) throws Throwable {
            // 매개변수를 전달받습니다. (helloA)
            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,..)") // @Before를 사용
        public void logArgs3(String arg) { // 타입으로 String으로 제한
            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()); // 어노테이션 값 출력
        }
    }
}

 

출력결과

 

참고

  • this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
  • target: Target 객체(스프링 AOP 프록시가 가리키는 실제 대상)를 대상으로 하는 조인 포인트

즉, this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭하고, target은 실제 target 객체를 대상으로 포인트컷을 매칭합니다. (* 같은 패턴을 사용할 수 없고, 부모 타입을 허용합니다.)

 

소스 코드

https://github.com/eunhwa99/SpringAOP/blob/main/advanced/src/test/java/hello/aop/pointcut/ParameterTest.java

 

SpringAOP/advanced/src/test/java/hello/aop/pointcut/ParameterTest.java at main · eunhwa99/SpringAOP

SpringAOP 구현 코드. Contribute to eunhwa99/SpringAOP development by creating an account on GitHub.

github.com

 

 


@annotation을 활요한 실전 예제 코드도 아래에 첨부하였습니다. 

 

예외 발생 시 재시도를 하는 애너테이션(@Retry)와 메소드 호출 정보가 출력되는 편리한 로그를 출력하는 애너테이션(@Trace)을 이용하여 스프링 AOP를 적용한 코드입니다. (advanced/src/test/java/hello/aop/exam/ExamTest.java 코드를 실행하여 동작을 확인할 수 있습니다.)

 

https://github.com/eunhwa99/SpringAOP/tree/main/advanced/src/main/java/hello/aop/exam

 

SpringAOP/advanced/src/main/java/hello/aop/exam at main · eunhwa99/SpringAOP

SpringAOP 구현 코드. Contribute to eunhwa99/SpringAOP development by creating an account on GitHub.

github.com

 


[참고자료]

김영한, "스프링 핵심 원리 - 고급편 ", 인프런

 

728x90
반응형