Spring

Spring 프록시 팩토리

제리 . 2022. 3. 13. 14:23

스프링은 JDK Dynamic Proxy, CGLIB의 프록시 생성 패턴을 사용한다.

이전 글에서 소개한 것 처럼 JDK Dynamic Proxy는 리플렉션을 사용하며 인터페이스가 반드시 필요했다.

반면, CGLIB는 바이트 코드를 조작해서 프록시를 만들고 상속을 사용하기 때문에 구체 클래스만으로도 프록시 생성이 가능했다.

 

하지만, 매번 코드상의 인터페이스 유무를 확인해서 JDK Dynamic Proxy의 InvocationHandler, CGLIB의 MethodInterceptor를 구현하기 쉽지 않다. 또한, 프록시 로직이 같을 때 인터페이스 유무가 다르다면 같은 내용의 InvocationHandler, MethodInterceptor를 작성해야 한다. 꽤 번거롭다.

 

스프링의 프록시 팩토리는 프록시 생성에 관해 추상화를 해준다.

  1. 인터페이스 유무에 따라 JDK Dynamic Proxy, CGLIB방식을 선택해준다. 물론, 옵션을 변경하여 프록시 생성 방식을 조절할 수도 있다.
  2. InvocationHandler, MethodInterceptor를 한 단계 추상화해준 어드바이스라는 것을 제공한다. 어드바이스에 프록시 로직을 한 번만 작성하면 된다.

 

프록시 팩토리를 사용하면 어떤 방식으로  프록시를 생성할지 적절하게 선택해준다.

클라이언트는 어떤 방식으로 프록시를 구현할지 관심이 없다. 프록시 팩토리에 요청하면 적절한 프록시 생성 방식을 통해 반환해준다.

 

프록시 팩토리는 advice를 통해 InvocationHandler, MethodInterceptor를 추상화한다.

InvocationHandler, MethodInterceptor는 Advice를 호출하고 있다.

어떤 방식으로 프록시가 만들어졌건 advice를 호출해서 사용하기때문에 advice에 로직을 한 번만 정의해주면 된다.

 

프록시 팩토리를 사용하는 예제

프록시의 로직을 담는 advice를 작성하자. advice를 구현하는 단순한 방법으로는 MethodInterceptor인터페이스를 구현하면 된다.

import org.aopalliance.intercept.MethodInterceptor

class TimeAdvice : MethodInterceptor {

    private val logger = LoggerFactory.getLogger(this::class.simpleName)

    override fun invoke(invocation: MethodInvocation): Any? {
        val startTime = System.currentTimeMillis()
        val result = invocation.proceed()
        val endTime = System.currentTimeMillis()
        logger.info("걸린 시간 : ${endTime - startTime} Millis")
        return result
    }
}

MethodInterceptort의 invoke() 메서드를 override 해주면 advice구현이 끝난다.

 

그리고 JDK Dynamic Proxy로 프록시가 생성되는 것을 확인하기 위해 인터페이스가 있는 클래스를 만들었다.

class AImpl : AInterface {

    private val logger = LoggerFactory.getLogger(this::class.simpleName)

    override fun call(): String {
        logger.info("A 호출")
        return "data"
    }
}

테스트에서 프록시를 호출한다.

@Test
fun `프록시 팩토리 테스트(JDK Dynamic Proxy)`() {
    val target = AImpl()
    val proxyFactory = ProxyFactory(target)
    proxyFactory.addAdvice(TimeAdvice())
    val proxy = proxyFactory.proxy as AInterface
    proxy.call()
    logger.info("real class : ${target.javaClass}")
    logger.info("proxy class : ${proxy.javaClass}")
    logger.info("isProxy? ${AopUtils.isAopProxy(proxy)}")
    logger.info("isJdkDynamicProxy? ${AopUtils.isJdkDynamicProxy(proxy)}")
    logger.info("isCglibProxy? ${AopUtils.isCglibProxy(proxy)}")
}

 

1. 프록시 팩토리에 타깃을 넘겨준다. 여기서는 인터페이스로 구현된 타겟을 넘겨주었다.

val proxyFactory = ProxyFactory(target)

2. advice를 추가해준다.

proxyFactory.addAdvice(TimeAdvice())

3. 프록시를 반환받고 메서드를 호출한다.

val proxy = proxyFactory.proxy as AInterface
proxy.call()

 

결과를 보면 TimeAdvice가 잘 적용돼서 메서드 실행에 걸린 시간이 출력되었고 JDK Dynamic Proxy로 만들어진 것 을 볼 수 있다.

 

이번에는 구체 클래스를 사용해서 프록시를 생성해보자.

open class B {

    private val logger = LoggerFactory.getLogger(this::class.simpleName)

    open fun call(): String {
        logger.info("B 호출")
        return "data"
    }
}
@Test
fun `프록시 팩토리 테스트(CGLIB)`() {
    val target = B()
    val proxyFactory = ProxyFactory(target)
    proxyFactory.addAdvice(TimeAdvice())
    val proxy = proxyFactory.proxy as B
    proxy.call()
    logger.info("real class : ${target.javaClass}")
    logger.info("proxy class : ${proxy.javaClass}")
    logger.info("isProxy? ${AopUtils.isAopProxy(proxy)}")
    logger.info("isJdkDynamicProxy? ${AopUtils.isJdkDynamicProxy(proxy)}")
    logger.info("isCglibProxy? ${AopUtils.isCglibProxy(proxy)}")
}

마찬가지로 프록시가 잘 적용됐고 CGLIB로 생성된 것을 확인할 수 있다.

 

인터페이스를 구현한 클래스여도 CGLIB방식을 적용하고 싶으면 다음과 같이 옵션을 주면 된다.

proxyFactory.isProxyTargetClass = true