본문 바로가기
Spring

[Spring] 스프링 포인트컷 지시자 정리

by 개미가되고싶은사람 2024. 10. 1.

목차

    포인트컷 표현식

    포인트컷(Pointcut)은 핵심 기능에 부가 기능을 추가할지 여부를 결정하는 필터링 역할을 합니다. 포인트컷 표현식은 이러한 부가 기능을 적용할 때 사용되는 기준을 정의합니다. 이 표현식은 패키지 이름, JDK 정규 표현식, AspectJ 표현식 등 여러 방법으로 설정할 수 있으며, 그중에서도 AspectJ 표현식이 가장 널리 사용됩니다.

     

     

     

    포인트컷 지시자 종류 

    포인트컷 지시자는 포인트컷 표현식을 정의할 때 어떤 방법을 이용해서 작성할 지 도와주는 표현입니다.

    포인트컷 표현식은 And, or, not(&&, ||, !)와 같은 논리 연산자를 함께 사용할 수 있으며 * 같은 표현도 사용할 수 있습니다.

     

    execution : 메소드 실행 조인 포인트를 매칭 (가장 많이 사용)
    within : 정확한 특정 타입 내의 조인 포인트들로 매칭을 제한
    args : 실제 넘어온 파라미터를 기반으로 객체 인스턴스에 대해 판단
    this : 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
    target : Target 객체(스프링 AOP 프록시가 적용되는 실제 객체)를 대상으로 하는 조인 포인트
    @target : 실행 객체의 클래스에 주어진 타입의 애노테이션이 있는 조인 포인트
    @within : 주어진 애노테이션이 있는 타입 내 조인 포인트
    @annotation : 메서드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭
    @args : 전달된 실제 인수의 런타임 타입이 주어진 타입의 애노테이션을 갖는 조인 포인트
    bean : 스프링 전용 포인트컷 지시자, 빈의 이름으로 포인트컷을 지정

     

     

    execution

    execution 문법

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

    modifiers-pattern = 접근제어자(생략 가능)

    ret-type-pattern = 반환타입

    declaring-type-pattern = 선언타입 (생략 가능)

    name-pattern = 메소드이름

    param-pattern = 파라미터

    throws-pattern = 예외 (생략 가능)

    예시) execution(public String hello.aop.member.MemberServiceImpl.hello(String)) - 
    예시) execution(* *(..)) - 최대 생략 표현

     

     

    간단한 예시로 execution 문법에 대해서 알아보겠습니다.

    --------- 메서드 이름 매칭 --------------
    // 모든 public 메서드 실행
    execution(public * *(..))
    
    // tes이름으로 시작하는 모든 메서드 실행
    execution(* tes*(..))
    
    --------- 패키지 경로 매칭 --------------
    // MemberServiceImpl에 정의된 모든 메서드 실행
    execution(* hello.aop.order.member.MemberServiceImpl.*(..))
    
    // member패키지에 정의된 클래스들의 메서드 실행
    execution(* hello.aop.order.member.*.*(..)))
    
    // member 하위 패키지에 정의된 클래스들의 메서드 실행
    execution(* hello.aop.order.member..*.*(..))
    
    --------------타입 매칭(부모 타입 허용)-------------
    // MemberServiceImpl의 부모 타입의 MemberSerivce
    //부모 타입을 선언한 메소드만 매칭 가능 즉 오버라이딩한 메소드만 가능
    execution(* hello.aop.order.member.MemberService.*(..))
    
    -----------파라미터-------------
    
    //String 타입의 파라미터 한 개만 허용
    execution(* *(String))
    
    //파라미터가 없는 경우
    execution(* *())
    
    //정확히 하나의 파라미터 허용, 모든 타입 허용
    execution(* *(*))
    
    //파라미터 갯수과 상관없이 모든 타입의 파라미터 허용
    //파라미터가 없어도 허용
    execution(* *(..))
    
    //String 타입으로 시작, 모든 타입의 파라미터 허용
    // (String), (String, xxx), (String, xxx, xxx) 가능
    execution(* *(String, ..))
    • . : 정확하게 해당 위치의 패키지
    • .. : 해당 위치의 패키지와 그 하위 패키지도 포함

     

    within

    within은 특정(정확한) 타입에 대해 매칭하는 지시자로 해당 타입이 매칭되면 그 안의 메서드(조인 포인트)들이 자동으로 매칭됩니다. 즉 execution에서 선언 타입(declaring-type-pattern) 부분만 작성하면 된다고 생각하면 됩니다.

    within을 사용할 때 주의해야 할 점은 execution과는 다르게 부모, 자식 타입에 상속 관계를 고려하지 않는다는 점입니다. 이는 within의 특징상 정확하게 타입이 맞아야 하기 때문입니다.

    // 부모 = MemberSerivce 자식 = MemberServiceImpl이고, hello 메소드를 오버라이딩한 상황
    
    public class WithinTest {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        Method helloMethod;
    
        @BeforeEach
        public void init() throws NoSuchMethodException {
            helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
        }
    
        @Test
        void withinTest1() {
            pointcut.setExpression("within(hello.aop.order.member.MemberService)");
            assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
        }
    
        @Test
        void withinTest2() {
            pointcut.setExpression("within(hello.aop.order.member.MemberServiceImpl)");
            assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
        }
    }

     

    결과

     

     

    args

    args는 실제 넘어온 파라미터를 기반 판단하며, execution의 기본 문법과 동일합니다. 이 두 지사지의 차이점은 execution 지사자가 메서드의 파라미터 타입이 정확히 일치해야 매칭되는 반면, args 지사자는 부모 타입을 허용한다는 점입니다. 즉, execution은 특정 타입의 인자만 사용할 수 있지만, args는 자식 클래스의 인스턴스도 부모 클래스의 타입으로 매칭할 수 있습니다.

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

     

     

    그 외 포인트컷 지시자 사용 예시 및 설명

    // @Transactional 애너테이션이 있는 인스턴스의 모든 메서드를 조인 포인트로 적용 
    // 즉, 부모 타입의 어드바이스(메소드) 적용
    @target(org.springframework.transaction.annotation.Transactional)
    
    // @Transactional 애너테이션이 있는 해당 타입 내에 있는 메서드만 조인 포인트로 적용
    // 즉 부모 타입의 어드바이스(메소드) 적용x
    @within(org.springframework.transaction.annotation.Transactional)
    
    // 실행 메서드에 @Transactional 애너테이션이 있는 조인 포인트
    @annotation(org.springframework.transaction.annotation.Transactional)
    
    // 전달된 실제 인수의 런타임 타입이 주어진 타입의 @Classified 애너테이션을 갖는 조인 포인트
    @args(com.xyz.security.Classified)
    
    // AccountService !!프록시 객체!!가 구현하는 모든 메소드
    this(com.xyz.service.AccountService)
    
    // AccountService !!실제 객체!!가 구현하는 모든 메소드
    target(com.xyz.service.AccountService)
    
    // tradeService 라는 이름을 가진 스프링 빈의 모든 메소드
    bean(tradeService)
    
    // *Service 라는 이름을 가진 스프링 빈의 모든 메소드
    bean(*Service)

     

     

    args, @args, @target 사용 시 주의사항

    args, @args, @target은 실행 시점에서 어드바이스가 적용될 수 있는지를 판단합니다. 이러한 판단은 기본적으로 프록시 객체가 필요합니다. 그러나 특정 상황에서는 실제 객체에 대한 프록시 객체가 존재하지 않을 수 있습니다. 이 경우, 스프링은 모든 빈에 대해 프록시를 생성하려고 시도합니다.

     

    그러나 이 과정에서 final로 선언된 빈들 때문에 오류가 발생할 수 있습니다. 스프링은 기본적으로 CGLIB를 사용하여 프록시 객체를 생성하는데, final 클래스나 메서드는 CGLIB에 의해 프록시 객체를 생성할 수 없기 때문에 문제가 발생합니다.

     

    따라서 args, @args, @target을 사용할 때는 해당 객체에 프록시 객체가 제대로 생성되는지를 확인하는 것과 execution과 같은 다른 지시자를 사용해 범위를 줄이는 방법이 중요합니다.