[Spring]동적 프록시 - JDK 동적 프록시, CGLIB
섹션 5. 동적 프록시 기술
!중요! 동적 프록시를 공부하기 전 자바의 리플렉션을 알아야 한다
어제 공부한 내용을 간단하게! 정리해 보자.
프록시를 생성할 때 interface가 있다면 JDK 동적 프록시로 생성, 인터페이스가 없고 구체 클래스만 있다면 CGLIB로 생성한다.
JDK 동적 프록시
JDK 동적 프록시(java.lang.reflect.Proxy)의 경우 프록시가 수행하는 로직을 handler라고 한다. 정확히는 프록시가 수행하길 원하는 특정 로직을 어떤 클래스에 구현했을 때 이 클래스는 InvocationHandler라는 인터페이스를 구현해야 한다.
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@Slf4j
public class TimeInvocationHandler implements InvocationHandler {
private final Object target;
public TimeInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
//메서드 동적 호출
Object result = method.invoke(target, args); //args : 메서드 호출 시 넘겨줄 인수
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
위 코드에서 볼 수 있는 것처럼 해당 인터페이스에는 invoke() 메서드가 있다. 이 메서드가 바로 프록시가 수행할 로직이 되며, method.invoke()는 target 인스턴스의 메서드를 호출한다.
import hello.proxy.jdkdynamic.code.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;
@Slf4j
public class JdkDynamicProxyTest {
@Test
void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
JDK 동적 프록시를 생성하는 방법은 간단하다. 우선 프록시가 수행할 handler를 만들고 target 클래스를 인수로 넘겨준다. 그다음 Proxy.newProxyInstance(프록시를 만들 클래스로더(ClassLoader), 프록시의 기반이 될 인터페이스, 프록시가 호출해야 하는 로직(InvocationHandler의 구현체))를 사용하여 프록시를 생성한다. 이때 생성되는 프록시의 타입은 Object지만, 인수로 넘겨준 인터페이스를 기반으로 하기 때문에 타입 캐스팅을 하여 타입을 변경한다.
ClassLoader 부분은 내 추측이지만, 런타임에 동적으로 로드할 인터페이스를 넘기면 되는 것 같다. (이 부분에 정확히 어떤 클래스 로더를 넘겨주는 건지 대해 찾아봤지만, 아직 설명하는 글을 못 봤다)
프록시를 생성했으면 이를 이용하여 target의 메서드를 실행한다. (call(): AImpl에서 오버라이드한 메서드)
실행 결과를 보면 TimeProxy.invoke()의 실행과 해당 메소드 안의 method.invoke()를 통해 AImpl.call()을 호출하는 것을 확인할 수 있다. 또한 마지막 줄에 com.sun.proxy.$Proxy9을 볼 수 있는데 이는 JDK 동적 프록시로 생성된 프록시를 의미한다(마지막 숫자는 달라진다).
CGLIB
CGLIB(org.spring.framework.cglib.proxy.*)도 JDK 동적 프록시와 비슷하다. CGLIB는 handler가 아닌 MethodInterceptor 인터페이스를 구현해야 한다. 여기서 흥미로운 점은 MethodInterceptor에 들어가 보면 CallBack을 상속받고 있는 것을 확인할 수 있다.
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, 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;
}
}
JDK 동적 프록시와 비슷하게 여기서는 invoke() 대신 intercept()를 수행하고, 이 안에서 methodProxy.invoke()를 이용하여 target 인스턴스의 메서드를 호출한다.
import hello.proxy.cglib.code.TimeMethodInterceptor;
import hello.proxy.common.service.ConcreteService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.cglib.proxy.Enhancer;
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
}
CGLIB는 Enhancer라는 것을 사용하여 생성된다. Enhancer.setSuperclass()에는 프록시의 기반이 될 구체 클래스를 담고, Enhaner.setCallback()에는 프록시가 수행해야 할 로직을 담는다(콜백패턴이 생각난다). 그다음 Enhancer.create()로 프록시를 생성한다. 이렇게 생성하면 Object 타입이 반환되는데 구체 클래스의 타입으로 타입캐스팅을 해주자.
동작 방식은 JDK 동적 프록시와 비슷하다. 이렇게 CGLIB로 프록시를 만들면 프록시 이름에 $$EnhancerByCGLIB$$xxx 라고 붙는다.