공부/Spring

[Spring] 스프링 AOP 포인트컷

작은소행성 2024. 5. 21. 22:15

포인트컷 지시자 

AspectJ 는 포인트컷을 편리하게 표현하기 위해 특별한 표현식을 사용한다. 

포인트컷 표현식은 AspectJ가 제공하는 포인트컷 표현식을 줄여서 말하는 것으로

execution 같은 포인트컷 지시자(Pointcut Designator, PCD)로 시작한다. 

 

지시자 종류 

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

execution 을 가장 많이 사용하고 나머지는 자주 사용하지 않는다. 

 

예제 

 @Target(ElementType.TYPE) // 클래스에 붙인 어노테이션
 @Retention(RetentionPolicy.RUNTIME) // 실제 어플리케이션이 실행(runtime)할때까지 해당 어노테이션이 살아있음
 public @interface ClassAop { 
}
 @Target(ElementType.METHOD)// 메서드에 붙일 수 있는 어노테이션
 @Retention(RetentionPolicy.RUNTIME) // 실제 어플리케이션이 실행(runtime)할때까지 해당 어노테이션이 살아있음
 public @interface MethodAop {
     String value();
}
 public interface MemberService {
     String hello(String param);
}
 @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";
     } 
  }
 @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() { // MemberServiceImpl.hello(String) 메서드의 정보 출력
         // public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
         log.info("helloMethod={}", helloMethod);
     }
}

 

 

@Test
 void exactMatch() {
     //public java.lang.String hello.aop.member.MemberServiceImpl.hello(java.lang.String)
     pointcut.setExpression("execution(public String hello.aop.member.MemberServiceImpl.hello(String))");
     // 해당 결과가 true로 반환되야 매칭됨
     assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue(); // (메서드, 대상 클래스)
 }

 

매칭 조건이 정확하게 일치해야 반환된다. 

  • public / String / hello.aop.member.MemberServiceImpl / hello / (String) 

 

execution 

// 모든 메서드
execution(* *(..))

// 메서드 이름 매칭 : hel, el 이름으로 시작하는 메서드 실행
execution(* hel*(..))
execution(* *el*(..))

// . : 정확하게 해당 위치의 패키지, member패키지에 정의된 메서드 실행
execution(* hello.aop.member.*.*(..))

// .. : 해당 위치의 패키지와 그 하위 패키지도 포함, aop 혹은 그 하위 패키지에 정의된 메서드 실행 
execution(* hello.aop..*.*(..))

// 부모 타입을 선언해도 자식 타입은 매칭됨 (MemberServiceImpl 의 부모 MemberService)
// MemberServiceImpl 에 선언한 internal 는 안됨)
execution(* hello.aop.member.MemberService.*(..))

// 파라미터 매칭
//String 타입의 파라미터 허용
execution(* *(String))

// 숫자와 무관하게 모든 파라미터, 모든 타입 허용, 파라미터가 없어도 됨
execution(* *(*))

// String 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용
execution(* *(String, ..))

 

execute 파라미터 매칭 규칙 

  • (String) : 정확하게 String 타입 파라미터
  • () : 파라미터가 없어야 함
  • (*) : 정확히 하나의 파라미터, 단 모든 타입을 허용
  • (*, *) : 정확히 두개의 파라미터, 단 모든 타입을 허용 
  • (..) : 숫자와 무관한 모든 파라미터, 모든 타입 허용, 파라미터가 없어도 되는데 없으면 0..*로 이해하면 된다. 
  • (String, ..) : String 타입으로 시작해야함, 숫자와 무관하게 모든 파라미터, 모든 타입 허용

 

within 

특정 타입이 매칭이 되면 그 안의 메서드들이 자동으로 매칭된다. 

문법은 단순한데 execution 에서 타입 부분만 사용한다고 보면 된다. 

execution과 다른점은 within사용 시 표현식에 부모타입을 지정하면 안된다. 정확하게 타입이 맞아야 한다. 

within은 하나만 지정할 수 있다보니 잘 사용안하고 execution 을 더 잘 사용한다. 

within(hello.aop.member.MemberServiceImpl)
within(hello.aop.member.*Service*)
within(hello.aop..*)

 

 

args

기본 문법은 execution의  args 부분과 같다. 

둘의 차이점으로는

execution는 파라미터 타입이 정확하게 매칭되어야 하며, 클래스에 선언된 정보를 기반으로 판단한다. 

args 는 부모 타입을 허용하고 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다. 

args는 단독으로 사용되기 보다 파라미터 바인딩에서 주로 사용된다. 

/**
 * execution(* *(java.io.Serializable)): 메서드의 시그니처로 판단 (정적) 
 * args(java.io.Serializable): 런타임에 전달된 인수로 판단 (동적)
 */
    @Test
    void argsVsExecution() {
        //Args
        assertThat(pointcut("args(String)") // String 은 Object, java.io.Serializable 의 하위 타입
                .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();
}

 

주의할 점으로는 args, @args, @target은 단독으로 사용하면 안된다. 

 

@target, @within

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

@target, @within 도 파라미터 바인딩에서 함께 사용된다. 

//@target: 인스턴스 기준으로 모든 메서드의 조인 포인트를 선정, 부모 타입의 메서드도 적용
@Around("execution(* hello.aop..*(..)) && @target(hello.aop.member.annotation.ClassAop)")


//@within: 선택된 클래스 내부에 있는 메서드만 조인 포인트로 선정, 부모 타입의 메서드는 적용 되지 않음
@Around("execution(* hello.aop..*(..)) && @within(hello.aop.member.annotation.ClassAop)")

 

 

@annotation, @args

@annotation 은 메서드가 주어진 어노테이션을 가지고 있는 조인포인트를 실행한다. 

다음과 같이 메서드(조인 포인트)에 어노테이션이 있으면 매칭한다. 

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

 

// MethodAop 가 있는곳에 적용
@Around("@annotation(hello.aop.member.annotation.MethodAop)")
public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
 }

 

bean

스프링 빈의 이름으로 AOP 적용 여부를 지정한다. 

// orderService, *Repository(OrderRepository) 의 메서드에 AOP가 적용됨
@Around("bean(orderService) || bean(*Repository)")

 

 

매개변수 전달

this, target, args,@target, @within, @annotation, @args 를 사용해서 어드바이스에 매개변수를 전달할 수 있다. 

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

 

 

this, target

적용 타입 하나를 정확하게 지정해야 한다. 

* 패턴을 사용할 수 없고 부모 타입을 허용한다. 

this(hello.aop.member.MemberService)
target(hello.aop.member.MemberService)

두개의 차이는 무엇일까

실제 target 객체 대신에 프록시 객체가 스프링 빈으로 등록된다. 

 

 

 

반응형