Spring 핵심원리 고급편 - CGLIB

목차

참고

Dynamic Proxy 의 한계와 CGLIB

Dynamic Proxy 의 경우 인터페이스를 구현한 클래스에 대해서만 프록시 객체를 생성할 수 있습니다. 그렇다면 인터페이스가 없는 클래스의 경우 프록시 생성을 못하냐? 정답은 아닙니다.

자바에서는 인터페이스를 구현하지 않은 클래스에 대해서도 프록시 객체를 생성할 수 있도록 CGLIB 를 제공합니다. 차이점은 Dynamic Proxy 는 인터페이스 구현을 통해 프록시 객체를 생성하지만 CGLIB 는 상속 을 통해 프록시 객체를 생성한다는 것입니다.

CGLIB 란?

CGLIB 는 Code Generation Library의 약자로 런타임 시 Java 바이트 코드 를 조작해 동적으로 클래스를 생성하는 라이브러리다. JDK Dynamic 프록시와는 다르게 구체 클래스 만 갖고도 동적으로 Proxy 를 생성할 수 있다.

CGLIB은 자바의 리플렉션(Reflection) API바이트코드 조작(Bytecode Manipulation) 기술을 이용하여 클래스의 상속 구조를 이용해서 프록시 객체를 생성합니다. 이 과정에서 바이트코드를 조작하므로, JVM의 클래스 로딩 과정에서 원본 클래스의 바이트코드를 변경할 수 있습니다.

CGLIB을 이용하여 프록시 객체를 생성할 때는 Enhancer 클래스를 이용합니다. Enhancer 클래스는 동적으로 클래스를 생성할 때 필요한 정보를 받아서 CGLIB이 제공하는 Callback 인터페이스 를 이용해서 프록시 객체를 생성합니다.

MethodInterceptor 인터페이스 - 부가기능 로직 작성

CGLIB 는 MethodInterceptor 를 이용해 JDK Dynamic Proxy 의 InvocationHandler 처럼 공통 로직 을 작성한다.

MethodInterceptor 인터페이스의 intercept 메소드는 프록시 객체가 호출될 때, 요청을 가로채 호출된 메소드 대신 실행됩니다. 이를 이용하여, 메소드 호출 전후에 로그를 남기거나, 메소드 호출 시간을 측정하거나, 트랜잭션을 관리하는 등의 작업을 수행할 수 있습니다.

MethodInterceptor 인터페이스는 자바의 다이나믹 프록시에서 제공하는 InvocationHandler와 유사한 역할을 합니다. 그러나 CGLIB은 상속 기반의 프록시 객체를 생성하므로, MethodInterceptor를 이용하여 원본 클래스의 메소드를 호출할 때, super 키워드를 이용하여 부모 클래스의 메소드를 호출할 수 있습니다.

  • Object object : CGLIB 가 적용된 객체
  • Method method : 호출된 메소드
  • Object[] args : 메서드를 호출하면서 전달된 인수
  • MethodProxy proxy : 메서드 호출에 사용하는 객체
public interface MethodInterceptor extends Callback {
Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}

부가기능 로직 작성

method.invoke(target, args) 를 이용해 원본 클래스의 메소드를 호출하던 방식과 달리, CGLIB 은 methodProxy.invokeSuper(target, args) 를 이용해 원본 클래스의 메소드를 호출합니다.

method.invoke(target, args) 로 실제 메소드를 호출해도 로직은 작동하지만 methodProxy.invokeSuper(target, args) 방식으로 호출하는게 성능상의 이접이 있다고 하니 참고하도록 하자!

@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {

private final Object target;


public TimeMethodInterceptor(Object target) {
this.target = target;
}

// 이 메소드는 세 개의 인자를 받습니다.
// 첫 번째 인자는 프록시 객체 자신입니다.
// 두 번째 인자는 원본 객체의 메소드입니다.
// 세 번째 인자는 원본 객체의 메소드 호출 시 전달된 인자 배열입니다.
@Override
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();

// 원본 클래스의 실제 메소드를 호출합니다.
Object result = methodProxy.invoke(target, args);

long endTime = System.currentTimeMillis();

long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime = {}", resultTime);

return result;
}
}

Enhancer - 프록시 객체 생성

CGLIB 에서는 Enhancer 를 이용해 Proxy 객체를 생성한다.

원본 클래스

@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}

CGLIB 프록시 객체 생성 테스트 코드

CGLIB 에서 프록시 객체를 생성하고자 할 때, Enhancer 를 이용해 프록시 객체를 생성합니다.

Enhancer 객체 setSuperclass 메소드를 이용해 프록시 객체를 생성할 원본 클래스를 지정하고 setCallback 메소드를 이용해 프록시 객체에 부가기능로직이 구현된 MethodInterceptor 객체를 지정합니다.

Enhancer 객체에 원본 클래스와 부가기능이 정의되면 create 메소드를 이용하여 프록시 객체를 생성할 수 있습니다.

@Test
void cglib(){
ConcreteService target = new ConcreteService();

// Enhancer 를 사용해 Proxy 를 생성한다.
Enhancer enhancer = new Enhancer();

// CGLIB 는 상속을 통해 구체 클래스 프록시를 생성할 수 있습니다.
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));

// setSuperclass 에서 지정한 클래스를 이용해 Proxy 객체를 생성한다.
ConcreteService proxy = (ConcreteService) enhancer.create();

log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());

// Proxy 객체를 통해 메소드를 호출하면, 부가기능이 실행됩니다.
proxy.call();
}

테스트 결과

Enhancer 클래스를 통해 CGLIB 프록시 객체가 생성된 것을 확인할 수 있습니다. 프록시 객체의 메소드가 호출되면, 부가기능이 실행되는 것을 확인할 수 있습니다.

23:52:18.794 [Test worker] INFO hello.proxy.cglib.CglibTest - targetClass = class hello.proxy.common.service.ConcreteService
23:52:18.798 [Test worker] INFO hello.proxy.cglib.CglibTest - proxyClass = class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
23:52:18.798 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 실행
23:52:18.807 [Test worker] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
23:52:18.807 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 종료 resultTime = 9

런타임시 의존 관계

CGLIB 장단점

CGLIB 을 이용한 프록시 객체 생성 방식은 다이나믹 프록시보다 더 빠른 속도를 보이며, 인터페이스를 구현하지 않은 클래스에 대해서도 프록시 객체를 생성할 수 있습니다. 하지만, 바이트코드를 조작하기 때문에 보안 검사 등에서 문제가 될 수 있습니다. 그리고 원본 클래스의 생성자나 private 메서드 등은 프록시 객체에서 호출할 수 없습니다.

Share