Spring

[Spring] 스프링 핵심 원리 - 고급편 섹션11 스프링 AOP - 포인트컷

고쩡이 2024. 8. 21. 17:49

본 내용은 인프런 김영한T 스프링 DB 2편 - 데이터 접근 활용 기술 강의를 공부하고 정리한 내용입니다:)

⬜섹션11 스프링 AOP - 포인트컷

◼️ AOP 개념

포인트컷 표현식: 에스펙트J가 제공하는 포인트컷 표현식

 

실습 사전 준비

- ClassAop, MethodAop, MemberService, MemberServiceImpl을 만든다. (상세 코드 in 교안)

@Slf4j
public class ExecutionTest {
    // AspectJExpressionPointcut; 포인트컷 표현식을 처리해주는 클래스
    AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
    Method helloMethod;

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

    @Test
    void printMethod() {
        log.info("helloMethod={}",helloMethod);
    }

    @Test
    void exactMatch() {
        pointcut.setExpression("execution(public String\n" +
                " hello.aop.member.MemberServiceImpl.hello(String))");
        assertThat(pointcut.matches(helloMethod,MemberServiceImpl.class)).isTrue();
    }
}

◼️ Exectuion

🟢 Execution 문법

execution(접근제어자? 반환타입 선언타입?메서드이름(파라미터) 예외?)

 

메서드 이름 앞 뒤에 *을 사용해서 매칭할 수 있다.

🔶 메서드 이름 매칭 관련 포인트컷

// 메서드 이름 매칭 관련 포인트컷
@Test
void nameMatch() {
    // 포인트컷 표현식 설정: 모든 리턴타입 + 메서드 이름이 정확히 "hello"인 메서드를 매칭
    pointcut.setExpression("execution(* hello(..))");
    
    // 설정한 포인트컷이 'helloMethod' 메서드와 매칭되는지 확인
    // MemberServiceImpl 클래스의 'helloMethod'가 포인트컷 표현식과 일치하는지 검증
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar1() {
    // 포인트컷 표현식 설정: 메서드 이름이 "hel"로 시작하는 모든 메서드를 매칭
    // '*'는 와일드카드로, "hel"로 시작하고 그 뒤에 어떤 글자가 와도 매칭
    pointcut.setExpression("execution(* hel*(..))");
    
    // 설정한 포인트컷이 'helloMethod' 메서드와 매칭되는지 확인
    // "hello"는 "hel"로 시작하기 때문에, 이 테스트는 true가 되어야 함
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

@Test
void nameMatchStar2() {
    // 포인트컷 표현식 설정: 메서드 이름에 "el"이 포함된 모든 메서드를 매칭
    // '*'는 와일드카드로, 앞이나 뒤에 어떤 글자가 와도 상관없음
    pointcut.setExpression("execution(* *el*(..))");
    
    // 설정한 포인트컷이 'helloMethod' 메서드와 매칭되는지 확인
    // "hello"는 "el"을 포함하기 때문에, 이 테스트는 true가 되어야 함
    assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
}

 

부모 타입을 표현식에 선언한 경우, 부모 타입에서 선언한 메서드가 자식 타입에 있어야 매칭에 성공한다. 아래에서 MemberServiceImpl안에는 internal메서드가 있어 매칭 대상이 되지만, MemberService에는 internal메서드가 없어 매칭에 실패한다.

    // 타입 매칭 - 부모 타입에 있는 메서드만 허용
    // 자식타입 MemberServiceImpl 을 표현식에 선언했기 때문에 그 안에 있는 internal(String) 메서드도 매칭 대상이 된다.
    @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();
    }

 

🟢 execution 파라미터 매칭 규칙

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

◼️ Within

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
    @DisplayName("타겟의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
    void withinSuperTypeFalse() {
        pointcut.setExpression("within(hello.aop.member.MemberService)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }

부모타입(ex_인터페이스) 지정시 within은 매칭이 실패하고, execution은 성공한다.

    @Test
    @DisplayName("타겟의 타입에만 직접 적용, 인터페이스를 선정하면 안된다.")
    void withinSuperTypeFalse() {
        pointcut.setExpression("within(hello.aop.member.MemberService)");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isFalse();
    }

    @Test
    @DisplayName("execution 은 타입 기반, 인터페이스를 선정 가능.")
    void executionSuperTypeTrue() {
        pointcut.setExpression("execution(* hello.aop.member.MemberService.*(..))");
        assertThat(pointcut.matches(helloMethod, MemberServiceImpl.class)).isTrue();
    }

 

🟢 within VS execution

within:

  • 범위 기반: 특정 클래스나 패키지 범위를 지정
  • 모든 메서드 매칭: 해당 클래스나 패키지의 모든 메서드가 매칭됨

execution:

  • 메서드 시그니처,타입 기반: 특정 메서드 시그니처에 따라 매칭
  • 정밀한 매칭: 메서드의 이름, 반환 타입, 파라미터 등에 따라 매칭을 설정할 수 있음

◼️ args

execution: 파라미터 타입이 정확하게 매칭되어야 한다.

args: 부모타입 허용, 실제 넘어온 파라미터 객체 인스턴스를 보고 판단한다.자바가 기본으로 제공하는 `String` 은 `Object` , `java.io.Serializable` 의 하위 타입이다.

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

 

정적으로 클래스에 선언된 정보만 보고 판단하는 `execution(* *(Object))` 는 매칭에 실패한다. 

동적으로 실제 파라미터로 넘어온 객체 인스턴스로 판단하는 `args(Object)` 는 매칭에 성공한다. (부모 타입 허용)

◼️ @target,@within

🟢 @target vs @within

@target

:

인스턴스의 모든 메서드를 조인 포인트로 적용

@within:해당 타입 내에 있는 메서드만 조인 포인트로 적용

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

    @Autowired
    Child child;

    @Test
    void success() {
        log.info("child Proxy={}", child.getClass());
        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();
        }
    }
}

◼️ @annotation, @args

@annotation: 메서드(조인 포인트)에 애노테이션이 있으면 매칭한다.

@Slf4j
@Import(AtAnnotationTest.AtAnnotationAspect.class)
@SpringBootTest
public class AtAnnotationTest {

    @Autowired
    MemberService memberService;

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

    // @annotation 지시자를 사용하여 특정 애노테이션이 달린 메서드에만 AOP 적용
    @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();
        }
    }
}

◼️ bean

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

실행 결과

◼️ 매개변수 전달

아래 포인트컷 표현식으로 advice에 매개변수 전달이 가능하다. 타입은 메서드에 지정한 타입으로 제한된다.

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

◼️ this, target

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

 

JDK 동적 프록시: 인터페이스가 필수이고, 터페이스를 구현한 프록시 객체를 생성한다.

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

 

this에서, JDK 동적 프록시는 인터페이스 기반이므로 구현 클래스를 알 수 없다.

CGLIB 프록시는 구현 클래스 기반으로 생성되므로 구현 클래스를 알 수 있다.