스프링 AOP - 포인트컷

땡글이·2023년 2월 1일
0

스프링 AOP

목록 보기
5/5

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

  • ex) @Pointcut("execution(* hello.aop.order..*(..))")

포인트컷 표현식은 AspectJ pointcut expression 즉 AspectJ 가 제공하는 포인트컷 표현식을 줄여서 말하는 것이다.

포인트컷 지시자

포인트컷 표현식은 execution 과 같은 포인트컷 지시자 (Pointcut Designator) 로 시작한다. 줄여서 PCD 라고도 한다. 포인트컷 지시자에는 여러 종류가 있는데 하나씩 알아보자.

execution 지시자

메서드 실행 조인 포인트를 매칭한다. 스프링 AOP에서 가장 많이 사용하고, 기능도 복잡하다. 다른 종류의 포인트컷 지시자는 execution 지시자 기능의 일부분을 가지고 있는 지시자들이다.

조인포인트란, 어드바이스가 적용될 수 있는 위치를 의미한다. 스프링 AOP에서는 메서드 실행 지점으로 제한된다. AspectJ는 여러 지점에 적용 가능하다.

execution 지시자 문법

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

execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)
  • 메서드 실행 조인포인트를 매칭한다.
  • ?는 생략할 수 있다.
  • *같은 패턴을 지정할 수 있다.

execution(public String hello.aop.member.MemberServiceImpl.hello(String)) 과 같은 표현식을 분석해보자.

  • 접근제어자? : public
  • 반환타입 : String
  • 선언타입? : hello.aop.member.MemberServiceImpl
  • 메서드이름 : hello
  • 파라미터 : (String)
  • 예외? : 생략

다른 예제로 execution(* *(..)) 를 분석해보자.

  • 접근제어자? : 생략
  • 반환타입 : *
  • 선언타입? : 생략
  • 메서드이름 : *
  • 파라미터 : (..)
  • 예외? : 생략

    * 은 아무 값이 들어와도 된다는 것을 의미하고, ..는 파라미터의 타입과 파라미터 수가 상관없다는 뜻이다.

그리고 execution 지시자는 표현식에 부모 타입을 선언해도 그 자식 타입은 매칭이 된다. 다형성에서 부모타입=자식타입이 할당가능한 맥락과 비슷하다.

execution 지시자 예제

이제 직접 아래 테스트코드를 보면서 문법을 익혀보자. 보다보면 감이 올 것이다. 아래 테스트코드는 모두 통과하는 테스트들이다.
그리고 테스트코드에서 사용되는 AspectJExpressionPointcut 클래스는 포인트컷 표현식을 처리해주는 클래스이다. AspectJExpressionPointcut 클래스는 상위에 Pointcut 인터페이스를 가진다.

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";
    }
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ClassAop {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodAop {
    String value();
}
@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() {
        // 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))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void allMatch() {
        pointcut.setExpression("execution(* *(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void nameMatch() {
        pointcut.setExpression("execution(* hello(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void nameMatchStar1() {
        pointcut.setExpression("execution(* hel*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }


    @Test
    void nameMatchStar2() {
        pointcut.setExpression("execution(* *el*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void nameMatchFalse() {
        pointcut.setExpression("execution(* nono(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }

    @Test
    void packageExactMatch() {
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.hello(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void packageExactMatch2() {
        pointcut.setExpression("execution(* hello.aop.member.*.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void packageExactFalse() {
        pointcut.setExpression("execution(* hello.aop.*.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }

	// . 는 정확히 해당 위치의 패키지를 의미함.
    // .. 은 해당 위치의 패키지와 그 하위 패키지도 포함
    @Test
    void packageExactMatchSubPackage1() {
        pointcut.setExpression("execution(* hello.aop.member..*.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void packageExactMatchSubPackage2() {
        pointcut.setExpression("execution(* hello.aop..*.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void typeExactMatch() {
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void typeMatchSuperType() {
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void typeMatchInternal() throws NoSuchMethodException {
        pointcut.setExpression("execution(* hello.aop.member.MemberServiceImpl.*(..))");
        Method internalMethod = MemberServiceImpl.class.getMethod("internal", String.class);

        assertThat(pointcut.matches(internalMethod, 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();
    }

    // String 타입의 파라미터 허용
    // (String)
    @Test
    void argsMatch() {
        pointcut.setExpression("execution(* *(String))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }


    @Test
    void argsMatchNoArgs() {
        pointcut.setExpression("execution(* *())");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }

    // 정확히 하나의 파리머터 허용, 모든 타입 허용
    // (A)
    @Test
    void argsMatchStar() {
        pointcut.setExpression("execution(* *(*))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    // 숫자와 무관하게 모든 파라미터, 모든 타입 허용
    // (), (A), (A, B)
    @Test
    void argsMatchAll() {
        pointcut.setExpression("execution(* *(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    // String 타입으로 시작, 숫자와 무관하게 모든 파라미터, 모든 타입 허용
    // (String), (String, A), (String, A, B)
    @Test
    void argsMatchComplex() {
        pointcut.setExpression("execution(* *(String, ..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }


}

within 지시자

within 지시자는 특정 타입 내의 조인포인트들로 매칭을 제한한다. 해당 타입이 매칭되면 그 안의 메서드들(조인포인트)이 자동으로 매칭된다. 문법은 단순하고, execution 에서 타입부분만 사용한다고 보면 된다.
주의해야할 점은 표현식에 부모 타입을 지정하면 안된다는 것이다. 정확하게 타입이 맞아야 매칭이 된다. 이것이 executionwithin의 차이점이다.

within 지시자 예제

바로 예제를 보면서 이해해보자.

public class WithinTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        helloMethod = MemberServiceImpl.class.getMethod("hello", String.class);
    }

    @Test
    void withinExact() {
        pointcut.setExpression("within(hello.aop.member.MemberServiceImpl)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void withinStar() {
        pointcut.setExpression("within(hello.aop.member.*Service*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void withinSubPackage() {
        pointcut.setExpression("within(hello.aop..*)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

    @Test
    void withinSuperTypeFalse() {
        pointcut.setExpression("within(hello.aop.member.MemberService)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }

    @Test
    void executionSuperTypeTrue() {
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }
}

@target, @within 지시자

@target 지시자는 실행 객체의 클래스에 주어진 타입의 어노테이션이 있는 조인포인트에 매칭되는 지시자를 의미한다. @within 지시자는 주어진 어노테이션이 있는 타인 내 조인포인트에 매칭되는 지시자이다.
그리고, @target 은 인스턴스의 모든 메서드를 조인 포인트로 적용한다. @within 은 해당 타입 내에 있는 메서드만 조인 포인트로 적용한다.

@Slf4j
@Import({AtTargetAtWithinTest.Config.class})
@SpringBootTest
public class AtTargetAtWithinTest {

    @Autowired
    Child child;

    @Test
    void success() {
        child.childMethod();
        child.parentMethod();
    }

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

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

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

    static class Parent {
        public void parentMethod() {}
    }

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

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

다음은 실행결과이다.

[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@within] void hello.aop.pointcut.AtTargetAtWithinTest$Child.childMethod()
[@target] void hello.aop.pointcut.AtTargetAtWithinTest$Parent.parentMethod()

실행결과를 보면, @within 지시자로 매칭되는 메서드에 Parent 클래스의 parentMethod()는 적용되지 않는 것을 확인할 수 있다. 이는 Child 클래스에 parentMethod()가 정의되어 있지 않기 때문이다.

@annotation, @args

@annotation 지시자는 메서드가 주어진 어노테이션을 가지고 있는 조인포인트를 매칭한다.

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

    public String internal(String param) {
        return "ok";
    }
}
@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

    @Test
    void success() {
        memberService.hello("helloA");
    }

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

    }
}
[@annotation] String hello.aop.member.MemberServiceImpl.hello(String)

@args 지시자는 전달된 실제 인수의 런타임 타입이 주어진 타입의 어노테이션을 갖는 조인 포인트와 매칭된다. 잘 쓰는 지시자는 아니라 사용법에 대해선 생략한다.

bean 지시자

스프링 전용 포인트컷 지시자이다. 빈의 이름으로 매칭된다.

@Slf4j
@Import(BeanTest.BeanAspect.class)
@SpringBootTest
public class BeanTest {

    @Autowired
    OrderService orderService;

    @Test
    void success() {
        orderService.orderItem("itemA");
    }

    @Aspect
    static class BeanAspect {
        @Around("bean(orderService) || bean(*Repository)")
        public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[bean] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}
[bean] void hello.aop.order.OrderService.orderItem(String)
[orderService] 실행
[bean] String hello.aop.order.OrderRepository.save(String)
[orderRepository] 실행

this, target 지시자

this 지시자는 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인포인트와 매칭된다. target 지시자는 실제 객체(스프링 AOP 프록시가 가르키는 실제 대상)을 대상으로 하는 조인포인트와 매칭된다. 두 지시자 모두 부모타입을 허용한다.

두 개의 지시자는 스프링에서 프록시를 생성하는 기술에 따라 조심히 사용해야 한다. JDK 동적 프록시는 인터페이스를 기반으로 프록시를 생성하고, CGLIB 는 구체 클래스를 기반으로 프록시를 생성한다.

JDK 동적 프록시 기술을 이용했을 경우

MemberService 인터페이스 지정

this(hello.aop.member.MemberService)

  • 프록시 객체를 보고 판단한다. this 지시자는 부모 타입을 허용하므로 AOP 적용 가능

target(hello.aop.member.MemberService)

  • 실제 객체를 보고 판단한다. target은 부모 타입을 허용하므로 AOP 적용 가능

MemberServiceImpl 구체 클래스 지정

this(hello.aop.member.MemberServiceImpl)

  • 프록시 객체를 보고 판단한다. 인터페이스를 기반으로 생성된 프록시 객체는 구체 클래스에 대해 알지 못하므로 AOP 적용 불가

target(hello.aop.member.MemberServiceImpl)

  • 실제 객체를 보고 판단한다. 실제 객체가 MemberServiceImpl 타입이므로 AOP 적용 가능

CGLIB 기술을 이용했을 경우

MemberService 인터페이스 지정

this(hello.aop.member.MemberService)

  • 프록시 객체를 보고 판단한다. this 지시자는 부모 타입을 허용하므로 AOP 적용 가능

target(hello.aop.member.MemberService)

  • 실제 객체를 보고 판단한다. target은 부모 타입을 허용하므로 AOP 적용 가능

MemberServiceImpl 구체 클래스 지정

this(hello.aop.member.MemberServiceImpl)

  • 프록시 객체를 보고 판단한다. CGLIB 기술로 만들어진 프록시 객체는 구체 클래스(MemberServiceImpl) 를 상속받아 생성되었기 때문에 AOP 적용 가능

target(hello.aop.member.MemberServiceImpl)

  • 실제 객체를 보고 판단한다. 실제 객체가 MemberServiceImpl 타입이므로 AOP 적용 가능

this, target 지시자 예제

@Slf4j
@Import(ThisTargetTest.ThisTargetAspect.class)
//@SpringBootTest(properties = "spring.aop.proxy-target-class=false") //JDK 동적 프록시
@SpringBootTest(properties = "spring.aop.proxy-target-class=true") //CGLIB
public class ThisTargetTest {

    @Autowired
    MemberService memberService;

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

    @Slf4j
    @Aspect
    static class ThisTargetAspect {

        //부모 타입 허용
        @Around("this(hello.aop.member.MemberService)")
        public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        //부모 타입 허용
        @Around("target(hello.aop.member.MemberService)")
        public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("this(hello.aop.member.MemberServiceImpl)")
        public Object doThis(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        @Around("target(hello.aop.member.MemberServiceImpl)")
        public Object doTarget(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

}

JDK 동적 프록시 기술 사용했을 때의 실행결과

[target-impl] String hello.aop.member.MemberService.hello(String)
[target-interface] String hello.aop.member.MemberService.hello(String)
[this-interface] String hello.aop.member.MemberService.hello(String)

CGLIB 기술 사용했을 때의 실행결과

[target-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[target-interface] String hello.aop.member.MemberServiceImpl.hello(String)
[this-impl] String hello.aop.member.MemberServiceImpl.hello(String)
[this-interface] String hello.aop.member.MemberServiceImpl.hello(String)

@SpringBootTest(properties = "spring.aop.proxy-target-class=false") 라면, 프록시를 생성할 때 JDK 동적 프록시 기술을 사용한다. @SpringBootTest(properties = "spring.aop.proxy-target-class=true") 라면, CGLIB 기술을 사용한다. application.properties 나 application.yml 파일에 spring.aop.proxy-target-class 속성을 명시해주는 방법도 있다.

Reference

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard
https://lifere.tistory.com/entry/Spring-AOP-args%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%ED%8C%8C%EB%9D%BC%EB%AF%B8%ED%84%B0-%EC%B6%94%EC%A0%81

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글