java/spring

[Spring] Pointcut 유형에 따라 Proxy 생성 방식이 달라진다? (CGLIB or JDK Proxy)

danuri 2024. 11. 2. 18:00

"토비의 스프링 3.1 Vol.1 > 6.5.3 포인트컷 표현식을 이용한 포인트컷"을 공부하면서 배운 부분을 기록.

 

여기 프록시 관련 빈 설정 파일이 있다.

@Configuration
class BeanConfig {

    @Bean
    fun userService(): UserService = UserServiceImpl(userLevelUpgradePolicy(), userDao())

    @Bean
    fun defaultAdvisorAutoProxyCreator(): DefaultAdvisorAutoProxyCreator = DefaultAdvisorAutoProxyCreator()

    @Bean
    fun transactionAdvisor(): DefaultPointcutAdvisor =
        DefaultPointcutAdvisor(transactionPointcut(), transactionAdvice())


    @Bean
    fun testTransactionPointcut(): NameMatchClassMethodPointcut {
        val pointcut = NameMatchClassMethodPointcut()
        pointcut.setMappedClassName("*ServiceImpl")
        pointcut.setMappedName("upgrade*")
        return pointcut
    }

    @Bean
    fun transactionAdvice(): TransactionAdvice = TransactionAdvice(transactionManager())
}

빈 설정 파일을 간단히 설명하면,

1. DefaultAdvisorAutoProxyCreator를 빈 등록하면 Advisor를 구현한 빈을 탐색한다. (예제에서 DefaultPointcutAdvisor)

2. Advisor는 Pointcut(예제에서 NameMatchClassMethodPointcut, 프록시 적용 대상 범위)에 해당하는 모든 빈(예제에서 UserServiceImpl)에 대해 Advice(예제에서 TransactionAdvice, 프록시 적용 내용)를 적용한다.

3.  간단히 요약하면, UserServiceImpl 빈에 트랜잭션 기능을 부여하는 프록시를 생성한다는 것이다.

 

이제 실제로 어떤 프록시 클래스가 생성되는지 출력해보자.

@Test
fun advisorAutoProxyCreator() {
    println(userService.javaClass.name)
}

// 결과
UserServiceImpl$$SpringCGLIB$$0

CGLIB 프록시가 생성됐다.

 

근데 여기서 의아한 점은 UserServiceImpl는 UserService 인터페이스를 구현했는데 CGLIB 프록시가 생성됐다는 것이다.

일반적으로 알려진 지식은 이렇다.

 

https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html

 

1. 타깃이 하나 이상의 인터페이스를 구현하고 있으면 JDK Proxy를 생성

2. 타깃이 인터페이스를 구현하지 않았다면 CGLIB을 생성

-> 그렇다면 UserServiceImpl은 JDK Proxy를 생성하는게 맞는 것 같다.

 

의문이 들어서 이래저래 검색해보니 Pointcut에 원인이 있었다.

사실 Pointcut은 스프링이 제공하는 NameMatchMethodPointcut을 구현한 클래스를 직접 정의했었다.

class NameMatchClassMethodPointcut : NameMatchMethodPointcut() {

    fun setMappedClassName(mappedClassName: String) {
        classFilter = SimpleClassFilter(mappedClassName)
    }

}

class SimpleClassFilter(
    private val mappedName: String,
) : ClassFilter {

    override fun matches(clazz: Class<*>): Boolean  = PatternMatchUtils.simpleMatch(mappedName, clazz.simpleName)

}

 

기본적으로 메서드 기반 매칭을 제공하는 NameMatchMethodPointcut을 확장해서 클래스 기반으로도 매칭할 수 있게끔 구현했다.

 

그리고 이렇게 구현하는 것은 다음과 같이 동작한다.

  • NameMatchClassMethodPointcut은 단순 문자열("*ServiceImpl") 기반 클래스 매칭이기 때문에 프록시 생성 시 클래스 이름을 기반으로 한 매칭을 수행한다.. 이로 인해 CGLIB 프록시 생성이 기본적으로 사용된다.
  • Spring은 일반적으로 인터페이스를 통한 프록시를 생성하는 것이 효율적이라고 판단하지만, 클래스 매칭 기반으로 지정되면 JDK Proxy보다는 CGLIB 방식이 선호된다.

 

그럼 내가 직접 구현한 클래스 매칭 Pointcut이 아닌 스프링이 제공하는 포인트컷 표현식을 사용해서 Pointcut을 만들어봤다.

@Bean
fun transactionPointcut(): AspectJExpressionPointcut {
    val pointcut = AspectJExpressionPointcut()
    pointcut.expression = "execution(* *..*ServiceImpl.upgrade*(..))"
    return pointcut
}

 

AspectJExpressionPointcut은 클래스와 메서드 적용 범위를 포인트컷 표현식을 이용해 한 번에 지정할 수 있게 해준다.

+) 아마 애노테이션 기반 스프링 AOP를 적용할 때 많이 봤던 표현식일 것이다.

 

이제 다시 테스트를 돌려본다.

@Test
fun advisorAutoProxyCreator() {
    println(userService.javaClass.name)
}

// 결과
jdk.proxy2.$Proxy95

-> JDK Proxy가 잘 생성된 것을 볼 수 있다.

이는 스프링이 기본 방식으로 JDK Proxy를 선택하고, 인터페이스가 없으면 CGLIB을 생성하는 우리가 잘 아는 방식을 그대로 따르고 있다.

 


 

실제로 실무에서 프록시의 유형이 중요한 코드를 작성할 일이 있을지 모르겠지만,

스프링은 기본적으로 JDK Proxy를 우선한다는 점,

그럼에도 불구하고 Pointcut에 따라 인터페이스를 구현한 객체에도 CGLIB 프록시를 적용할 수도 있다는 점

-> 이 정도 기억해두면 되지 않을까 싶다.