0+ 스프링/0+ 스프링 AOP

[스프링 AOP] 포인트컷 지시자

힘들면힘을내는쿼카 2023. 2. 3. 20:56
728x90
반응형

2023.01.30 - [0 + 프로그래밍/0 + SpringBoot(스프링부트)] - [스프링] AOP 개념

 

[스프링] AOP 개념

[스프링] AOP 개념 스프링 핵심 원리 - 고급편 - 인프런 | 강의 이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다. 스프링 핵심 원리 - 고급편 - 인프런 | 강의 스프링의

howisitgo1ng.tistory.com

2023.01.31 - [0 + 프로그래밍/0 + SpringBoot(스프링부트)] - [스프링] AOP 실습

 

[스프링] AOP 실습

[스프링] AOP 실습 스프링 핵심 원리 - 고급편 - 인프런 | 강의 이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다. 스프링 핵심 원리 - 고급편 - 인프런 | 강의 스프링의

howisitgo1ng.tistory.com

위 AOP 개념과 실습 포스팅을 읽고 오면 더욱 맛있게 글을 읽을 수 있습니다.^^

[스프링] AOP 포인트컷 지시자

스프링 핵심 원리 - 고급편 - 인프런 | 강의
이 글은 인프런에서 스프링 핵심 원리 - 고급편 강의를 참고하여 작성했습니다.

 

스프링 핵심 원리 - 고급편 - 인프런 | 강의

스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

포인트컷 지시자에 대해서 알아봅시다.
AspectJ는 포인트컷을 편리하게 표현하기 위한 특별한 표현식을 제공합니다.

이번 포스팅은 내용이 다소 재미없을 수 있습니다… ㅠㅠ
필요할때 찾아봐야쥐~~  같은 가벼운 마음으로 보시면 좋을 것 같습니다.^^

가보자 가보자!

넷플릭스 -수리남

 

포인트컷 지시자

먼저 가볍게(?) 포인트컷 지시자 종류에 대해서 설명해보겠습니다.

포인트컷 지시자 종류

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

이 중에서 가장 많이 사용하는 execution, @annotation, this, target에 대해서 설명드리겠습니다.

 

본격적인 설명에 들어가기에 앞서서 핵심기능 역할을 하는 예제 코드를 작성하겠습니다.

핵심기능 예제 코드

PaymentService

public interface PaymentService {
    String payment(int price);
}

PaymentServiceImpl

@Component
public class PaymentServiceImpl implements PaymentService {
    @Override
    public String payment(int price) {
        return "가격은 "+price+" 입니다.";
    }

    public String paymentChild(int price) {
        return price+" 자식입니다.";
    }
}

execution

execution 문법은 다음과 같습니다.

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

패키지 매칭 규칙

  • *은 아무 값이 들어와도 된다는 의미
  • .은 정확하게 해당 위치의 패키지
  • ..은 정확하게 해당 위치의 패키지

execution 파라미터 매칭 규칙

  • (..): 파라미터의 타입파라미터 수가 상관없다는 의미
  • (String): 정확하게 String 타입 파라미터
  • (): 파라미터가 없어야 함
  • (*): 정확히 하나의 파라미터, 단 모든 타입을 허용
  • (*, *): 정확하게 두 개의 파라미터, 단 모든 타입 허용
  • (String, ..): String 타입으로 시작하고, 숫자와 무관하게 모든 파라미터, 모든 타입을 허용

먼저 정확하게 모든 내용이 매칭되는 표현식부터 살펴보겠습니다.

가장 정확한 포인트컷

@Slf4j
@SpringBootTest
public class ExecutionPaymentTest {

    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method method;

    @BeforeEach
    public void init() throws NoSuchMethodException {
        method = PaymentServiceImpl.class.getMethod("payment", int.class);
    }

    @Test
    void exactMath() {
        // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
        pointcut.setExpression("execution(public String spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.payment(int))");
        Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
    }
}

매칭 조건

  • 접근제어자?: public
  • 반환타입: String
  • 선언타입?: pring.advanced.advancedspring.practice.payment.PaymentServiceImpl
  • 메소드이름: payment
  • 파라미터: (int)
  • 예외?: 없음

가장 많이 생략한 포인트컷

@Test
void allMatch() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* *(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}

매칭 조건

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

응용(메소드 이름 매칭)

@Test
void nameMatch() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* payment(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* *ment(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}

@Test
void nameMatchFalse() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* minkai(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isFalse();
}

응용(패키지 이름)

@Test
void packageExactMath1() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.payment(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}

@Test
void packageExactMatch2() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.*.*(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}

@Test
void packageExactSubPackage() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* spring.advanced.advancedspring.practice..*.*(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}

@Test
void packageExactFalse() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.*.*(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isFalse();
}

참고로 타입 매칭이 부모 타입도 허용됩니다.(PaymentServiceImpl의 부모타입은 PaymentService)

@Test
void typeMatchSuperType() {
    // execution(접근제어자? 반환타입 선언타입?메소드이름(파라미터) 예외?)
    pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentService.*(..))");
    Assertions.assertThat(pointcut.matches(method, PaymentServiceImpl.class)).isTrue();
}

executionPaymentService처럼 부모타입을 선언해도 자식타입(PaymentServiceImpl)은 매칭 됩니다.
(다형성을 떠올리면 쉽게 이해가 되실거에요^^)

부모타입에 자식타입의 메소드가 없는 경우를 생각해야 합니다.

@Test
void typeMatchChild() throws NoSuchMethodException {
    pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.*(..))");
    Method childMethod = PaymentServiceImpl.class.getMethod("paymentChild", int.class);
    Assertions.assertThat(pointcut.matches(childMethod, PaymentServiceImpl.class)).isTrue();
}

// PaymentService에는 paymentChild()라는 메소드가 존재하지 않음
@Test
void typeMatchChildFalse() throws NoSuchMethodException {
    pointcut.setExpression("execution(* spring.advanced.advancedspring.practice.payment.PaymentService.*(..))");
    Method childMethod = PaymentServiceImpl.class.getMethod("paymentChild", int.class);
    Assertions.assertThat(pointcut.matches(childMethod, PaymentServiceImpl.class)).isFalse();
}

typeMatchChild()의 경우 PaymentServiceImpl를 표현식에 선언했기 때문에 그안에 있는 paymentChild(int) 메소드도 매칭 대상이 됩니다.

그런데 typeMatchChildFalse()여기를 주의해서 보시기 바랍니다. 👀
typeMatchChildFalse() 경우 PaymentService을 표현식에 선언했기 때문에 PaymentServiceImplpaymentChild(int)메소드는 매칭 대상이 될수 없습니다…! PaymentService에는 paymentChild(int)가 없기 때문입니다.!!!!!!!!!!!!!!!!!!!

@annotation

@annotation: 메소드가 주어진 애노테이션을 가지고 있는 조인 포인트를 매칭

말로는 이해가 되지 않으니 코드로 보시죠!

메소드(조인 포인트)에 애노테이션이 있으면 매칭

MyMethodAop

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME) // 애노테이션의 라이프사이클
public @interface MyMethodAop {
    String value();
}

먼저 애노테이션을 작성합니다.

PaymentServiceImpl

@Component
public class PaymentServiceImpl implements PaymentService {

    @MyMethodAop("테스트 입니다.") // 애노테이션 추가
    @Override
    public String payment(int price) {
        return "가격은 "+price+" 입니다.";
    }

    public String paymentChild(int price) {
        return price+" 자식입니다.";
    }
}

그리고 메소드에 애노테이션을 추가 합니다.

AnnotationPaymentTest

@Slf4j
@SpringBootTest
@Import(AnnotationPaymentTest.AtAnnotationAspect.class)
public class AnnotationPaymentTest {
    @Autowired
    PaymentService paymentService;

    @Test
    void success() {
        log.info("memberService={}", paymentService.getClass());
        paymentService.payment(1000);
    }

    @Slf4j
    @Aspect
    static class AtAnnotationAspect {
        @Around("@annotation(spring.advanced.advancedspring.practice.payment.annotation.MyMethodAop)")
        public Object doAtAnnotation(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[@annotation] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

결과

memberService=class spring.advanced.advancedspring.practice.payment.PaymentServiceImpl$$EnhancerBySpringCGLIB$$ae59ea0c
[@annotation] String spring.advanced.advancedspring.practice.payment.PaymentServiceImpl.payment(int)

this, target

this: 스프링 빈 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트
target: target 객체(스프링 AOP 프록시)를 대상으로 하는 조인 포인트

this, target은 다음과 같이 적용 타입 하나를 정확하게 지정해야 합니다.

this(spring.advanced.advancedspring.practice.payment.PaymentService) 
target(spring.advanced.advancedspring.practice.payment.PaymentService) 

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

this는 스프링 빈으로 등록되어 있는 프록시 객체를 대상으로 포인트컷을 매칭
target은 실제 target 객체를 대상으로 포인트컷을 매칭

스프링은 프록시를 생성할 때 JDK 동적 프록시CGLIB를 선택할 수 있습니다.

JDK 동적 프록시

인터페이스가 필수이고, 인터페이스를 구현한 프록시 객체 생성

MemberService 인터페이스 지정

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

    @Autowired
    PaymentService paymentService;

    @Test
    void test() {
        log.info("paymentService Proxy={}", paymentService.getClass());
        paymentService.payment(100);
    }

    @Slf4j
    @Aspect
    static class ThisTargetAspect {

        /**
         * JDK Proxy 객체를 보고 판단 합니다.
         * this는 부모타입을 허용하기 때문에 AOP가 적용됩니다.
         */
        @Around("this(spring.advanced.advancedspring.practice.payment.PaymentService)")
        public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        /**
         * target 객체(PaymentServiceImpl)를 보고 판단 합니다.
         * target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
         */
        @Around("target(spring.advanced.advancedspring.practice.payment.PaymentService)")
        public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

MemberServiceImpl 구체 클래스 지정

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

    @Autowired
    PaymentService paymentService;

    @Test
    void test() {
        log.info("paymentService Proxy={}", paymentService.getClass());
        paymentService.payment(100);
    }

    @Slf4j
    @Aspect
    static class ThisTargetAspect {

        /**
         * JDK Proxy 객체를 보고 판단 합니다.
         * JDK Proxy 객체는 PaymentService를 상속받아서 생성하기 때문에
         * PaymentServiceImpl를 알지 못합니다.
         * 따라서 AOP 적용 대상이 아닙니다.
         */
        @Around("this(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
        public Object doThisConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-concrete] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        /**
         * target 객체(PaymentServiceImpl)를 보고 판단 합니다.
         * target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
         */
        @Around("target(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
        public Object doTargetConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-concrete] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

CGLIB

인터페이스가 있어도 구체 클래스를 상속 받아서 프록시 객체를 생성

MemberService 인터페이스 지정

@Slf4j
@SpringBootTest
@Import(ThisTargetTest.ThisTargetAspect.class)
public class ThisTargetTest {

    @Autowired
    PaymentService paymentService;

    @Test
    void test() {
        log.info("paymentService Proxy={}", paymentService.getClass());
        paymentService.payment(100);
    }

    @Slf4j
    @Aspect
    static class ThisTargetAspect {

        /**
         * CGLIB Proxy 객체를 보고 판단 합니다.
         * this는 부모타입을 허용하기 때문에 AOP가 적용됩니다.
         */
        @Around("this(spring.advanced.advancedspring.practice.payment.PaymentService)")
        public Object doThisInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        /**
         * target 객체(PaymentServiceImpl)를 보고 판단 합니다.
         * target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
         */
        @Around("target(spring.advanced.advancedspring.practice.payment.PaymentService)")
        public Object doTargetInterface(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-interface] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

MemberServiceImpl 구체 클래스 지정

@Slf4j
@SpringBootTest
@Import(ThisTargetTest.ThisTargetAspect.class)
public class ThisTargetTest {

    @Autowired
    PaymentService paymentService;

    @Test
    void test() {
        log.info("paymentService Proxy={}", paymentService.getClass());
        paymentService.payment(100);
    }

    @Slf4j
    @Aspect
    static class ThisTargetAspect {

        /**
         * CGLIB Proxy 객체를 보고 판단 합니다.
         * CGLIB Proxy 객체는 PaymentServiceImpl를 상속받아서 생성하기 때문에
         * AOP 적용 대상 입니다.
         */
        @Around("this(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
        public Object doThisConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-concrete] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        /**
         * target 객체(PaymentServiceImpl)를 보고 판단 합니다.
         * target은 부모타입을 허용하기 때문에 AOP가 적용됩니다.
         */
        @Around("target(spring.advanced.advancedspring.practice.payment.PaymentServiceImpl)")
        public Object doTargetConcrete(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-concrete] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }
}

this, target 정리
JDK 동적 프록시 생성 방식인지, CGLIB 프록시 생성 방식인지에 따라서 this의 경우 결과가 다를수 있다는 것을 알아둡시다…! 🤗

 

 

 

728x90
반응형

'0+ 스프링 > 0+ 스프링 AOP' 카테고리의 다른 글

[스프링 AOP] 프록시 타입 캐스팅 한계  (0) 2023.02.07
[스프링 AOP] 내부 호출 문제 해결  (0) 2023.02.07
[스프링 AOP] 실습  (0) 2023.01.31
[스프링 AOP] 개념  (0) 2023.01.30