Spring

[Spring] 포인트컷 지시자 (AspectJ Pointcut Expression) - 2

이덩우 2024. 5. 10. 22:31

직전 포스팅에서는 포인트컷 지시자 중 `execution`, `within`, `args`에 관한 문법에 대해 알아봤다.

이들은 표현식을 사용해 대상 타입이나 메서드 이름, 파라미터 타입 등을 지정했다.

 

이번 포스팅은 대상 타입만을 지정하는 `this`와 `target`지시자에 대해 알아볼 것이다.

 

1. 차이점

기본적으로 `this`와 `target` 모두 대상 타입을 지정한다는 점에서 공통점이 있다.

단순히 타입 하나를 지정하면 되는데 차이점은 뭘까?

 

`this`는 스프링 빈으로 등록되어 있는 *프록시 객체*를 대상으로 포인트컷 매칭을 시도한다.

`target`은 *실제 타겟*을 대상으로 포인트컷 매칭을 시도한다.

이게 무엇을 의미할까?

 

JDK 동적 프록시를 사용한 경우
CGLIB 프록시를 사용한 경우

 

우선 기본적으로 스프링은 별도의 설정이 없다면 CGLIB를 기반으로 프록시를 생성해 스프링 빈으로 등록한다.

 

이전 강의들을 떠올려보면, CGLIB는 *구체 클래스를 상속받은 프록시 객체*를 생성해 스프링 빈으로 등록한다.

이와 반면, JDK 동적 프록시는 *인터페이스를 상속받은 프록시 객체*를 생성해 스프링 빈으로 등록한다.

이것이 중요한 차이이다.

 

그림을 보면 실제 타겟은 당연히 구체 클래스를 의미한다. 프록시 내부 target 필드는 구체 클래스를 참조하고있기 때문이다.

이 같은 이유로, 실제 타겟을 대상으로 포인트컷을 매칭하는 `target`지시자는 포인트컷 *표현식에 구체 타입을 적든, 부모 타입을 적든 상관없이 매칭된다.* 실제 타겟, 즉 구체 클래스는 부모를 상속받고 있기 때문에 표현식에 부모 타입을 작성해도 *다형성*에 의해 인식이 가능하다.

 

이와 반면, 프록시 객체를 대상으로 포인트컷을 매칭하는 `this`지시자는 CGLIB를 사용하냐, JDK 동적 프록시를 사용하냐에 따라 결과가 달라진다. 

 

먼저 CGLIB를 사용하는 프록시라면, 구체 클래스를 상속받아 프록시 객체가 만들어진다. 그렇다면 포인트컷 표현식에 구체 클래스를 넣던, 부모 타입의 인터페이스를 넣던 상관없이 매칭이 가능하다. 이 역시 다형성 덕분에 가능한 일이다.

 

하지만 JDK 동적 프록시를 사용한다면, 프록시 객체는 부모 타입의 인터페이스를 상속받아 생성된다. 이 경우, 구체 클래스 타입으로 포인트컷 매칭을 시도한다면 *AOP 적용이 안된다. 부모는 자식을 포함할 수 있지만, 반대는 안되기 때문이다.*

이런 이유로 스프링은 CGLIB를 통해 프록시를 생성하는 것을 기본값으로 만들었을까? 

 

 


2. 테스트 코드로 확인

@Slf4j
@SpringBootTest(properties = "spring.aop.proxy-target-class=false")
@Import(ThisTargetTest.ThisTargetAspect.class)
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();
        }

        // JDK 동적 프록시를 사용하면, 프록시에 인터페이스를 상속받은 객체가 생성되기 때문에 Impl 을 알지 못함
        // -> AOP 적용 안된다.
        @Around("this(hello.aop.member.MemberServiceImpl)")
        public Object doThisImpl(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[this-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }

        // 타겟은 Impl 로 지정되기 때문에 상관 없다.
        @Around("target(hello.aop.member.MemberServiceImpl)")
        public Object doTargetImpl(ProceedingJoinPoint joinPoint) throws Throwable {
            log.info("[target-impl] {}", joinPoint.getSignature());
            return joinPoint.proceed();
        }
    }

}

 

먼저 JDK 동적 프록시를 사용하도록 설정값을 넣고 실제 AOP 적용 여부를 확인해보자.

JDK 동적 프록시 결과

위에서 설명한 대로 `this` 지시자를 사용한 경우 + 구체 클래스 타입으로 표현식 지정한 경우 AOP가 적용되지 않았다.

 

다음은 CGLIB를 사용하도록 설정하고 확인해보자.

CGLIB 결과

프록시에 구체 클래스를 상속받은 덕분에 `this`지시자 + 구체 클래스 타입으로 표현식을 지정해도 AOP가 적용된 모습이다.

 

`this`지시자 + 구체 클래스 타입으로 포인트컷 표현식을 지정한다면 프록시 생성 전략에 따라 결과가 다르게 나올 수 있다는 점을 기억하자.

 

 

출처 : 인프런, 김영한의 스프링 핵심 원리 - 고급편