JDK Dynamic Proxy와 CGLIB

Photo by Sigmund on Unsplash

JDK Dynamic Proxy와 CGLIB

Spring AOP의 프록시 생성 방식에 대해 간략히 알아봅시다.

💡ProxyFactory

스프링 AOP의 ProxyFactory는 프록시를 생성하는 과정에서 타겟이 하나 이상의 인터페이스를 구현하고 있다면 JDK Dynamic Proxy 방식으로 프록시가 생성하고, 그게 아니라면 CGLIB 방식으로 프록시가 생성해주는 팩토리입니다.

클라이언트가 ProxyFactory로 생성된 프록시를 호출하면 JDK Dynamic Proxy의 경우 InvocationHandler의 invoke() 메서드가 실행되고, CGLIB의 경우 MethodInterceptor의 intercept() 메서드가 실행됩니다. 이후 Advice를 호출하면서 Target에 대한 부가기능을 적용시킬 수 있게 됩니다.

💡JDK Dynamic Proxy

public interface MyService {
    void doSomething();
}

@Service
public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        // do something
    }
}

MyServiceImpl의 doSomething() 메서드 호출 전후로 로그를 찍으려합니다. 여기서 디자인 패턴이나 Spring AOP를 쓰는 이유에 대해서 얘기할 수도 있겠지만 이 글에서는 다루지 않겠습니다. 사실 이 글을 쓰는 것 자체가 AOP에 대한 이해가 아닌, @Transactional 포스팅을 위한 빌드업이거든요.

우선 저희는 JDK Dynamic Proxy를 이용하여 프록시 객체를 생성해보겠습니다. JDK Dynamic Proxy는 Reflection API를 사용합니다. Proxy 클래스의 newProxyInstance로 프록시를 생성하는데, 아래와 같이 타겟의 인터페이스를 구현하는 프록시를 생성하게 됩니다.

public class DynamicInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MyService realSubject = new MyServiceImpl();
        log.debug("메서드 호출 이전");
        Object invoke = method.invoke(realSubject, args);
        log.debug("메서드 호출 이후");
        return invoke;
    }
}
MyService myService = (MyService) Proxy.newProxyInstance(
                MyService.class.getClassLoader(), // 클래스로더
                new Class[]{MyService.class}, // 타겟의 인터페이스
                new DynamicInvocationHandler() // 타겟의 정보가 포함된 Handler
        );

InvocationHandler는 함수형 인터페이스이기 때문에 람다식을 이용하여 인라인으로 작성할 수도 있습니다.

MyService myService = (MyService) Proxy.newProxyInstance(
                MyService.class.getClassLoader(), // 클래스로더
                new Class[]{MyService.class}, // 타겟의 인터페이스
                ((proxy, method, args) -> {
                    MyService realSubject = new MyServiceImpl();
                    log.debug("메서드 호출 이전");
                    Object invoke = method.invoke(realSubject, args);
                    log.debug("메서드 호출 이후");
                    return invoke;
                }) // 타겟의 정보가 포함된 Handler
        );
// myService.doSomething();

메서드 호출 이전
... Do something ...
메서드 호출 이후

myService의 doSomething() 메서드를 실행하게 되면, 메서드 호출 전후로 로그가 찍힙니다. 여기까지의 과정을 다시 한번 정리해봅시다.

  1. Client가 MyService 타입 객체의 doSomthing()을 호출합니다. Client는 자신이 호출한 타겟이 프록시인지 아닌지 신경 쓸 필요 없습니다.

  2. 동적으로 만들어진 프록시 객체는 Client가 요청한 메서드의 정보와 메서드의 파라미터를 InvocationHandler 구현체의 invoke() 메서드 파라미터로 전달하여 호출합니다.

  3. InvocationHandler의 invoke() 메서드에서 Advice(로깅)를 적용하여 Target(realSubject)을 호출한 후, 반환합니다.

주의해야 할 것은 해당 Proxy Bean을 DI 할 때, 반드시 인터페이스 타입으로 지정해 줘야 합니다. 왜냐하면 클래스 타입으로 DI를 하는 경우, UnsatisfiedDependencyException이 발생해버립니다.

@Controller
public class MyController {
  @Autowired
  private MyServiceImpl myServiceImpl; // Runtime Error!!
}


@Service
public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        // do something
    }
}

다른 객체지향적인 이유를 차치하고서라도, 이러한 런타임 에러를 피하기 위해선 의존성 주입 시 인터페이스 타입으로 지정해 주는 것이 좋습니다.

💡CGLIB (Code Generator Library)

CGLIB은 클래스의 바이트 코드를 조작하여 프록시 객체를 생성해주는 라이브러리입니다. org.springframework.cglib.proxy의 Enhancer 클래스를 이용하여 프록시를 생성할 수 있습니다.

@Service
public class MyService {
    public void doSomething() {
        // do something
    }
}

MyService의 doSomething() 메서드에 대한 호출을 가로채는 프록시 클래스를 만들어봅시다. Enhancer 클래스를 사용하면 Enhancer 클래스의 setSuperclass() 메서드를 사용하여 MyService 클래스를 동적으로 확장하여 프록시를 만들 수 있습니다.

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        log.debug("메서드 호출 이전");
        Object invoke = methodProxy.invokeSuper(o, objects);
        log.debug("메서드 호출 이후");
        return invoke;
    }
}
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class); // 타겟 클래스
enhancer.setCallback(new MyMethodInterceptor()); // 핸들러
MyService proxy = (MyService) enhancer.create(); // 프록시 생성

MethodInterceptor 또한 다음과 같이 함수형으로 사용할 수 있습니다.

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class); // 타겟 클래스
enhancer.setCallback((MethodInterceptor) (Object o, Method method, Object[] objects, MethodProxy methodProxy) -> {
    log.debug("메서드 호출 이전");
    Object invoke = methodProxy.invokeSuper(o, objects);
    log.debug("메서드 호출 이후");
    return invoke;
}); // 핸들러
// myService.doSomething();

메서드 호출 이전
... Do something ...
메서드 호출 이후

마찬가지로 메서드 호출 전후로 로그가 찍힙니다. CGLIB은 어떻게 프록시를 생성했는지 다시 정리해 봅시다.

  1. Client가 MyService 타입 객체의 doSomthing()을 호출합니다. Client는 자신이 호출한 타겟이 프록시인지 아닌지 신경 쓸 필요 없습니다.

  2. enhancer가 Target 클래스(MemberService)를 상속 받습니다.

  3. enhancer에서 setCallback()을 통해 intercept() 메서드 내부에서 Advice 로직을 적용하여 Target(MyService)을 호출한 후, 반환합니다.

💡JDK Dynamic Proxy vs CGLIB

CGLIB은 최초로 호출됐을 때 바이트 코드를 조작하고, 이후 호출 시 조작된 바이트 코드를 재사용하기 때문에 성능적으로 JDK Dynamic Proxy에 비해 우수합니다. 차이가 어느정도인지 자세히 알고 싶으시다면, Baeldung에서도 사용된 벤치마크를 참고해주세요.

💡인터페이스를 구현해도 CGLIB만 사용하는데요?

CGLIB은 다음과 같은 문제점을 갖고 있었습니다.

  1. CGLIB 의존성을 추가해야 함.

  2. default 생성자가 꼭 필요함.

  3. 생성자가 두 번 호출됨.

의존성을 추가해야 하는 부분은 Spring 3.2부터 Spring Core에 CGLIB을 포함시켰습니다. Spring 4.0부터 Objensis 라이브러리를 통해 default 생성자가 필요 없고, 생성자가 두 번 호출되던 문제를 해결했습니다.

// spring-boot-autoconfigure의 spring-configuraion-metadata.json
...
{
    "name": "spring.aop.proxy-target-class",
    "type": "java.lang.Boolean",
    "description": "Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).",
    "defaultValue": true
},

CGLIB이 갖고 있던 여러 문제점이 해결되었기 때문에, Spring boot 1.4 이상에서는 기본적으로 인터페이스를 구현하더라도 CGLIB으로 Proxy를 생성합니다. 성능이 더 우수하니깐요.

JDK 동적 프록시를 사용하시려면 @EnableAspectJAutoProxy 어노테이션의 proxyTargetClass 옵션을 false로 주시거나, 프로퍼티에서 spring.aop.proxy-target-class=false를 적용시켜주시면 됩니다.


🎯정리

  • 타겟이 하나 이상의 인터페이스를 구현하는 경우 JDK Dynamic Proxy를, 아닌 경우 CGLIB Proxy를 사용합니다.

  • CGlib은 바이트 코드를 조작하여 프록시를 생성해 주기 때문에 JDK Dynamic Proxy보다 성능이 좋습니다.

  • Spring Boot에서는 기본적으로 CGLIB을 사용합니다.


🔖참고