개발메모/간단정리

[간단정리] JDK Dynamic Proxy vs CGLib Proxy

99C0RN 2023. 1. 4. 16:46

개요

Java/Spring 환경에서 사용되는 프록시 중 JDK Dynamic Proxy와 CGLib Proxy에 대한 기초 지식과 차이를 알아보자.


프록시(Proxy)가 뭘까?

프록시(proxy)는 대리 또는 대리인이라는 뜻을 가졌다. 업무를 하며 프록시라는 용어를 처음 접했을 때는 뜻을 모르는 상태여서, 뭔가 단어 자체가 어렵게만 느껴졌었다.
ex) Proxy Server, Proxy Pattern, HA-Proxy 등..

 

지금은 어느정도 단어의 뜻과 일치하게, '무언가 대신하는 ~' 느낌으로 이해하기 시작하면서 조금 더 쉽게 프록시와 관련된 내용에 대해 어렵게만 느껴졌던 부분이 해소됐다.

해당 포스팅에서는 Java에서 프록시 패턴이 적용되어, 우리가 알게 모르게 사용하고 있던
JDK Dynamic Proxy와 CGLib Proxy에 대한 간단한 내용 정리와 비교를 주제로 다뤄볼 생각입니다.


JDK Dynamic Proxy

JDK 에서 제공하는 Dynamic Proxy는 1.3 버젼부터 생긴 기능이며, Interface를 기반으로 Proxy를 생성해주는 방식입니다.
그렇기 때문에 Interface를 강제화 한다는 단점이 있다

Dynamic Proxy는 Invocation Handler를 상속받아서 실체를 구현하게 되는데, 이 과정에서 특정 Object에 대해 Reflection을 사용하기 때문에 성능이 조금 떨어지는 단점이 있습니다.

  • proxy 생성을 위해 interface가 필요하다.
  • Reflection을 이용해 proxy를 생성한다.

 

* JDK Dynamic Proxy를 사용하여 Proxy 객체를 직접 생성 하는 방법

Object proxy = Proxy.newProxyInstance(ClassLoader       // 클래스로더
                                    , Class<?>[]        // 타깃의 인터페이스
                                    , InvocationHandler // 타깃의 정보가 포함된 Handler

 

* 간단한 예제 코드

public class ProxyTest {

    // 인터페이스
    interface Target {
        void print();
    }

    // 프록시를 적용할 구현체
    class TargetImpl implements Target {
        @Override
        public void print() {
            System.out.println("Hello!");
        }
    }

    // InvocationHandler 구현체
    class TestHandler implements InvocationHandler {

        private Object target;

        public TestHandler(final Object target) {
            this.target = target;
        }

        @Override
        public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
            System.out.println("[start] - method");
            Object result = method.invoke(target, args);
            System.out.println("[end] - method");
            return result;
        }
    }

    // Proxy 생성 및 호출 test
    @Test
    void test() {
        Target proxy = (Target) Proxy.newProxyInstance(
                ProxyTest.class.getClassLoader(),
                new Class[]{Target.class},
                new TestHandler(new TargetImpl())
        );
        proxy.print();
    }
}

CGLib(Code Generator Library) Proxy

CGLib Proxy는 Enhancer를 바탕으로 Proxy를 구현하는 방식입니다
이 방식은 JDK Dynamic Proxy와는 다르게 Reflection을 사용하지 않고, Extends(상속) 방식을 이용해서 Proxy화 할 메서드를 오버라이딩 하는 방식입니다. 하지만, 상속을 이용하므로 final이나 private와 같이 상속에 대해 오버라이딩을 지원하지 않는 경우에는 Aspect를 적용할 수 없다는 단점이 있다.

  • 바이트 코드를 조작해 프록시 생성
  • Hibernate의 lazy loading, Mockito의 모킹 메서드 등에서 사용


* CGLib Proxy를 사용하여 Proxy 객체를 직접 생성 하는 방법

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(타깃.class);
enhancer.setCallback(new MethodInterceptor의구현체());
Object proxy = enhancer.create();

 

* 간단한 예제 코드

public class ProxyTest {

    // 프록시를 적용할 구현체
    class TargetImpl {
        public void print() {
            System.out.println("Hello!");
        }
    }

    // Proxy 생성 및 호출 test
    @Test
    void test() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(TargetImpl.class);
        enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
            System.out.println("[start] - method");
            Object result = proxy.invoke(obj, args);
            System.out.println("[end] - method");
            return result;
        });
        TargetImpl proxy = (TargetImpl) enhancer.create();
        proxy.print();
    }
}

JDK Dynamic Proxy vs CGLib

두 방식의 차이는 인터페이스의 유무 로서, AOP의 타겟이 되는 클래스가 인터페이스를 구현하였다면 JDK Dynamic Proxy 사용, 구현하지 않았다면 CGLIB 방식을 사용한다. 사용자가 어떻게 설정하느냐에 따라서 인터페이스를 구현했다 하더라도 CGLIB 방식을 강제하거나 AspectJ를 사용할 수 있다.

 

Spring에서 사용하는 proxy
- Spring AOP에서는 기본적으로 JDK dynamic proxy를 사용한다. 다만, JDK dynamic proxy는 인터페이스가 있어야만 사용할 수 있기 때문에 인터페이스가 없는 경우에는 CGLIB proxy를 사용한다.

SpringBoot에서 사용하는 proxy
- Spring Boot에서는 설정을 통해 JDK dynamic proxy를 이용할지, CGLIB proxy를 이용할지 선택할 수 있다. Spring Boot 2.0부터는 디폴트 설정이 CGLIB proxy를 사용하도록 바뀌었다.

 

진작 CGLIB을 이용하지 않았을까?
- 왜 Spring은 SpringBoot 2.0을 거치고 나서야 CGLIB proxy를 디폴트로 설정해둔걸까? JDK dynamic proxy는 무조건 인터페이스가 있어야하고 Reflection을 사용하기 때문에 성능이 비교적 느리다고 알려져있다. 그렇다면 진작에 CGLIB을 디폴트로 설정해두었으면 좋았을걸 왜 나중에 적용했을까?

  • 오픈소스
    - 일단 CGLIB가 오픈 소스였던 것이 문제다. 신뢰하고 사용해도 될 정도로 검증할 시간이 필요했고 Spring에 내장되어있지 않아 별도로 의존성을 추가해야한다는 문제도 있었다. Spring 3.2버전부터 spring-core로 리패키징된 상태라 의존성을 추가할 필요가 없어졌다.
  • 디폴트 생성자 필요 & 생성자 중복 호출
    - 기존에는 CGLIB를 이용하면 디폴트 생성자가 필요했고 원본 객체의 생성자를 두 번 호출했다. 실제 빈을 생성할 때 한번, 프록시 생성을 위해 한번더. Spring 4.3부터는 Objenesis 라이브러리를 통해 생성되기 때문에 해당 현상이 개선되었다.