JDK Dynamic Proxy와 CGLIB Proxy에 대한 이해

Toggle Space Navigation Tree
Space Map

Proxy에 대하여..

Spring AOP는 두가지 Type의 Proxy를 지원하고 있다. 그 첫번째는 JDK의 Proxy 기능을 이용하는 것이고, 두번째 방법은 CGLIB의 Enhancer 클래스를 이용하는 것이다. 이 두가지 Proxy의 차이점을 이해하고 사용하는 Spring AOP를 제대로 사용하는 것이 될 것이다.

Proxy의 핵심적인 기능은 원하는 메써드가 호출(Invocation)될 때 이 메써드를 가로채어 우리가 원하는 특정 기능들을 추가할 수 있도록 지원하는 것이다.

JDK Proxy

JDK Proxy는 인터페이스에 대한 Proxy만을 지원하며, 클래스에 대한 Proxy를 지원할 수 없다는 것이 큰 단점이다. 자바에서 인터페이스를 사용하는 것이 좋은 설계임에는 틀림없지만 모든 애플리케이션에서 인터페이스를 사용한다는 제약을 두는 것은 좋지 않다. 특히 국내와 같이 인터페이스를 많이 사용하지 않고, 이에 대한 인식이 없는 상태에서 Proxy 기능을 사용하기 위하여 인터페이스를 사용해야 한다고 강요할 경우 개발자들에게 상당한 반발을 가져올 수 밖에 없다.

또한 이미 구현되어 있는 애플리케이션에 Proxy기능을 추가할 때 JDK Proxy를 사용한다면 클래스로 구현되어 있는 소스에서 인터페이스를 추출한 다음 Proxy를 적용할 수 밖에 없다는 단점이 있다. 이와 같이 클래스에 Proxy를 적용하고자 할 경우에는 CGLIB Proxy만을 사용해야 한다.

JDK Proxy가 가지는 또 하나의 단점은 Target 클래스에 Proxy를 적용할 때 PointCut에 정보에 따라 Advice되는 메써드와 그렇지 않은 메써드가 존재한다. 그러나 JDK Proxy를 사용할 경우 Target 클래스에 대한 모든 메써드 호출이 일단 JVM에 Intercept한 다음 Advice의 invoke 메써드를 호출하게 된다. 그 후에 이 메써드가 Advice되는 메써드인지 그렇지 않은지를 판단하게 된다. 이 과정에서 JVM에 의하여 Intercept한 다음 invoke 메써드를 호출할 때 JDK의 reflection을 이용하여 호출하게 되는것이다. 이는 Proxy를 사용할 때 실행속도를 상당히 저하시키는 원인이 된다.

Spring 프레임워크에서 JDK Proxy를 사용하고자 한다면 ProxyFactory의 setProxyInterfaces() 메써드에 사용할 인터페이스를 전달하면 JDK Proxy를 이용할 수 있다. 그러나 이 메써드를 통하여 인터페이스를 전달하지 않을 경우 기본적인 Proxy는 CGLIB Proxy가 된다.

CGLIB Proxy

CGLIB Proxy 또한 JDK Proxy처럼 Runtime시에 Target 메써드가 호출될 때 해당 메써드의 Advice적용 여부를 결정하게 된다. 그러나 CGLIB는 메써드가 처음 호출 되었을때 동적으로 bytecode를 생성하여 이후 호출에서는 재사용하는 과정을 거치게 된다. 이 같은 과정을 통하여 두번째 호출이후부터는 실행속도의 향상을 가져올 수 있는 방법을 사용하고 있다.

또한 CGLIB Proxy는 클래스에 대한 Proxy가 가능하다.

JDK Proxy와 CGLIB Proxy의 성능비교

ISimpleBean.java
public interface ISimpleBean {

    public void advised();
    public void unadvised();
    
}
SimpleBean.java
public class SimpleBean implements ISimpleBean {

    private long dummy = 0;
    
    public void advised() {
        dummy = System.currentTimeMillis();
    }

    public void unadvised() {
        dummy = System.currentTimeMillis();
    }
}
NoOpBeforeAdvice.java
import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class NoOpBeforeAdvice implements MethodBeforeAdvice {

    public void before(Method method, Object[] args, Object target) throws Throwable {
        // no-op
    }
}
TestPointcut.java
import java.lang.reflect.Method;

import org.springframework.aop.support.StaticMethodMatcherPointcut;

public class TestPointcut extends StaticMethodMatcherPointcut {

    public boolean matches(Method method, Class cls) {
        return ("advised".equals(method.getName()));
    }

}
ProxyPerfTest.java
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.ProxyFactory;
import org.springframework.aop.support.DefaultPointcutAdvisor;

public class ProxyPerfTest {

    public static void main(String[] args) {
        ISimpleBean target = new SimpleBean();

        Advisor advisor = new DefaultPointcutAdvisor(new TestPointcut(),
                new NoOpBeforeAdvice());

        runCglibTests(advisor, target);
        runCglibFrozenTests(advisor, target);
        runJdkTests(advisor, target);
    }

    private static void runCglibTests(Advisor advisor, ISimpleBean target) {
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);
             
        ISimpleBean proxy = (ISimpleBean)pf.getProxy();
        System.out.println("Running CGLIB (Standard) Tests");
        test(proxy);
    }
    
    private static void runCglibFrozenTests(Advisor advisor, ISimpleBean target) {
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);
        pf.setFrozen(true);
        
        ISimpleBean proxy = (ISimpleBean)pf.getProxy();
        System.out.println("Running CGLIB (Frozen) Tests");
        test(proxy);
    }
    
    private static void runJdkTests(Advisor advisor, ISimpleBean target) {
        ProxyFactory pf = new ProxyFactory();
        pf.setTarget(target);
        pf.addAdvisor(advisor);
        pf.setInterfaces(new Class[]{ISimpleBean.class});
        
        ISimpleBean proxy = (ISimpleBean)pf.getProxy();
        System.out.println("Running JDK Tests");
        test(proxy);
    }
    
    private static void test(ISimpleBean bean) {
        long before = 0;
        long after = 0;
        
        // test advised method
        System.out.println("Testing Advised Method");
        before = System.currentTimeMillis();
        for(int x = 0; x < 500000; x++) {
            bean.advised();
        }
        after = System.currentTimeMillis();;
        
        System.out.println("Took " + (after - before) + " ms");
        
        // testing unadvised method
        System.out.println("Testing Unadvised Method");
        before = System.currentTimeMillis(); 
        for(int x = 0; x < 500000; x++) {
            bean.unadvised();
        }
        after = System.currentTimeMillis();;
        
        System.out.println("Took " + (after - before) + " ms");
        
        // testing equals() method
        System.out.println("Testing equals() Method");
        before = System.currentTimeMillis(); 
        for(int x = 0; x < 500000; x++) {
            bean.equals(bean);
        }
        after = System.currentTimeMillis();;
        
        System.out.println("Took " + (after - before) + " ms");
        
        // testing hashCode() method
        System.out.println("Testing hashCode() Method");
        before = System.currentTimeMillis(); 
        for(int x = 0; x < 500000; x++) {
            bean.hashCode();
        }
        after = System.currentTimeMillis();;
        
        System.out.println("Took " + (after - before) + " ms");
        
        // testing method on Advised
        Advised advised = (Advised)bean;
        
        System.out.println("Testing Advised.getProxyTargetClass() Method");
        before = System.currentTimeMillis(); 
        for(int x = 0; x < 500000; x++) {
            advised.getProxyTargetClass();
        }
        after = System.currentTimeMillis();;
        
        System.out.println("Took " + (after - before) + " ms");
        
        System.out.println(">>>\n");
    }
}

output :
Running CGLIB (Standard) Tests
Testing Advised Method
Took 310 ms
Testing Unadvised Method
Took 60 ms
Testing equals() Method
Took 30 ms
Testing hashCode() Method
Took 20 ms
Testing Advised.getProxyTargetClass() Method
Took 20 ms
>>>

Running CGLIB (Frozen) Tests
Testing Advised Method
Took 191 ms
Testing Unadvised Method
Took 60 ms
Testing equals() Method
Took 30 ms
Testing hashCode() Method
Took 20 ms
Testing Advised.getProxyTargetClass() Method
Took 10 ms
>>>

Running JDK Tests
Testing Advised Method
Took 410 ms
Testing Unadvised Method
Took 271 ms
Testing equals() Method
Took 260 ms
Testing hashCode() Method
Took 70 ms
Testing Advised.getProxyTargetClass() Method
Took 131 ms
>>>

위 결과를 보면 Advised 메써드의 경우 JDK보다 CGLIB(Standard) Proxy를 사용할 경우 25%이상 더 빠른 것을 확인할 수 있다. Advised 메써드의 경우에는 25%정도이지만 다른 UnAdvised되는 메써드를 확인해 보면 그 차이는 상당히 크다는 것을 확인할 수 있다. CGLIB Proxy에 비해 JDK Proxy가 4배 이상 느린 것을 확인할 수 있다. 우리들이 모든 Object에서 가지는 equals()와 hashCode()에서도 비슷한 결과가 나타난 것을 확인할 수 있다.

이 같은 결과가 나타나는 가장 큰 이유는 JDK Proxy가 UnAdvised 메써드임에도 불구하고 매번 자바의 reflection을 이용하여 Advice의 invoke 메써드를 호출하기 때문이다.

Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. 8월 21, 2008

    Anonymous says:

    안녕하세요. 이 코드를 테스트 중에 있는데... advised.getProxyTargetClass(); 에서 에러가 났습니다. 저 메소드가 없...

    안녕하세요.
    이 코드를 테스트 중에 있는데...
    advised.getProxyTargetClass();
    에서 에러가 났습니다.
    저 메소드가 없다는...
    스프링 2.5.4을 사용 중입니다.
    지금 열심히 원인 파악 중입니다.
    제 실수면 죄송 합니다.