Proxy에 대한 이해 – 2. CGLib

CGLib 은 JDK Proxy 와 달리 Interface 가 없어도 Byte코드 조작을 통해 Class 를 직접 조작하여 Proxy 를 생성한다.
JDK Proxy 는 원본 객체가 구현한 Interface 에 대한 Proxy 를 만들어준다면, CGLib 을 이용하면 직접 Class 를 상속받아 Override 하는 방식으로 Proxy 를 만들어주게 된다.
하나씩 살펴보자.

CGLib

CGLib 은 구체 클래스를 Extends( 상속 ) 받아서 Proxy 를 구현한다. 따라서, interface 가 없는 클래스라도 Proxy 를 생성할 수 있다. 예를들어, ProxyService 라는 Interface 를 구현한 ProxyServiceImpl 이 있다면 CGLib 으로 Proxy를 구현할 때 ProxyService 라는 interface 가 아닌 구체 구현 클래스를 상속받는 식으로 구현된다.

public class ProxyServiceImpl implements ProxyService{
    ...
}

public class CGLibProxyService extends ProxyServiceImpl{
    ...
}

//Proxy 생성
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ProxyServiceImpl.class); // Super Class
enhancer.setCallback(MethodInterceptor); // InvocationHandler 와 동일한 역할.
Object proxy = enhancer.create();

이 과정에서 Enhancer 를 이용해서 Proxy 를 구현하게 되고, 이 과정에서 MethodInterceptor 라는 것을 요구하는데 이는 이전 글에서 살펴보았던 InvocationHandler 와 동일한 의미를 갖는다. 어떻게 생겼는지 Interface를 한번 살펴보자.

package net.sf.cglib.proxy;


public interface MethodInterceptor extends Callback
{
    /**
     * All generated proxied methods call this method instead of the original method.
     * The original method may either be invoked by normal reflection using the Method object,
     * or by using the MethodProxy (faster).
     * @param obj "this", the enhanced object
     * @param method intercepted Method
     * @param args argument array; primitive types are wrapped
     * @param proxy used to invoke super (non-intercepted method); may be called
     * as many times as needed
     * @throws Throwable any exception may be thrown; if so, super method will not be invoked
     * @return any value compatible with the signature of the proxied method. Method returning void will ignore this value.
     * @see MethodProxy
     */
    public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
                               MethodProxy proxy) throws Throwable;

}

생김새도 InovcationHandler 와 비슷하게 생겨 intercept 라는 단 하나의 Method 만 갖는 interface 이고, 그 파라미터로 해당 Method에 대한 정보를 받는다. 그 역할은 주석에도 자세하게 적혀있는 것 처럼 “All generated proxied methods call this method instead of the original method.” 즉, proxy 가 대체해서 호출할 그 함수를 작성하라는 얘기다.
즉, 이 Enhancer 는 구체 클래스를 상속 받으며 저 interceptor 에서 작성된 대로 수행하는 Proxy Instance 를 만들어준다.
그런데, 가장 큰 차이점은 JDK Proxy 는 Reflection API 를 이용해서 InvocationHandler에서 작성된 내용을 수행하는 것이고, CGLib 처음 한번만 바이트코드를 조작해 Proxy 객체를 생성한 뒤, 이후부터는 해당 객체를 재사용 하는 것이다.

그리고 주석에도 잘 나와있지만, 만약 java.lang.reflect.Method 를 이용해서 MethodInterceptor 를 사용하는것 보다 MethodProxy를 쓰는게 더 빠르니 이것을 사용하라고 얘기하고있다. 즉, Parmaeter 로 주어지는 java.lang.reflect 의 method.invoke( ) 를 사용해버리면 CGLib 으로 만든 Proxy 도 JDK Proxy 와 다를 바가 없어진다.

enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
    ...
    //return method.invoke(obj, args); -> Slow
    return proxy.invokeSuper(obj, args);
});

// MethodProxy.java
public Object invokeSuper(Object obj, Object[] args) throws Throwable {
    try {
        this.init();
        MethodProxy.FastClassInfo fci = this.fastClassInfo;
        return fci.f2.invoke(fci.i2, obj, args);
    } catch (InvocationTargetException var4) {
        InvocationTargetException e = var4;
        throw e.getTargetException();
    }
}

이 MethodProxy 의 invokeSuper 메소드를 살펴보면 FastClass 라는 바이트코드를 통해 조작된 객체를 통해 Reflection API 보다 더 빠르게 메소드를 호출하거나 필드에 접근할 수 있도록 해준다. 이런 부분들 때문에 CGLib 이 JDK Proxy 보다 빠르다는 성능상 이점을 갖게 된다.

이런식으로 Proxy 객체를 만들었을 때 JDK Proxy 와 가장 크게 차이나는 부분은 바로 Type과 관련된 부분이다. 예를들어 할인율을 정하는 정책과 관련된 DiscountPolcy interface 와 구현체 FixedRateDiscountPolicy 가 있다고 해보자.

public interface DiscountPolicy{
    int totalDiscountMoney(int price);
}

public class FixedRateDiscountPolicy implements DiscountPolicy {
    private int rate = 10;
    
    public FixedRateDiscountPolicy(int rate){
        this.rate = rate;
    }    

    @Override
    public int totalDiscountMoney(int price){
        return ( price / this.rate );
    }

}

이 예제에 대한 2가지 프록시를 만들어보자.

//JDK Proxy
DiscountPolicy discountPolicyInstance = new FixedRateDiscountPolicy(10);
InvocationHandler handler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method call");
        Object result = method.invoke(discountPolicyInstance, args);
        System.out.println("After method call");
        return result;
    }
};
//FixedRateDiscountPolicy 로 형변환 불가능.
DiscountPolicy jdkProxyInstance = (DiscountPolicy) Proxy.newProxyInstance(
        discountPolicyInstance.getClass().getClassLoader(),
        discountPolicyInstance.getClass().getInterfaces(),
        handler
);

//CGLib
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(FixedRateDiscountPolicy.class);
enhancer.setCallback(new MethodInterceptor() {
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("Before method call");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("After method call");
        return result;
    }
});
//Interface로도 변환 가능.
DiscountPolicy cglibDiscountPolicyProxy = (DiscountPolicy) enhancer.create(new Class[] {int.class}, new Object[] {10});
FixedRateDiscountPolicy cglibFixedPolicyProxy = (FixedRateDiscountPolicy) enhancer.create(new Class[] {int.class}, new Object[] {10});

JDK Proxy 로 만든 프록시의 경우 앞선 글에서 살펴봤지만 Interface 에 대한 Proxy를 만들어 주기 때문에 구체적인 구현 클래스인 FixedRateDiscountPolicy 로는 형변환 할 수 없다.
그러나 CGLib은 FixedRateDiscountPolicy를 상속받아 처리했기 때문에 구체 클래스인 FixedRateDiscountPolicy 로도, 인터페이스인 DiscountPolicy 로도 형변환할 수 있다.

문제점

다만 상속을 통해 Proxy Class 를 만들기 때문에 생기는 몇가지 문제점들이 있다.
1. Final 키워드를 붙혀놓으면 재정의할 수 없기 때문에 Proxy 를 만들 수 없다.
2. 상속받은 클래스를 생성하려면 부모클래스의 생성자도 호출해야 한다. 따라서, 만약 부모 클래스의 생성자가 private ( Factory를 이용하거나.. ) 으로 선언되어 있으면 Proxy 객체를 만들 수 없다.

그 외에 SuperClass의 생성자가 2번 호출된다는 문제도 가지고 있는데, 이 부분은 Proxy 마지막 3편에서 SpringFramework 에서 Proxy 를 어떤 방식으로, 어떻게 다루고 있는지 살펴보면서 확인한다.

Leave a Comment