Question
- JDK 동적 프록시가 동적으로 프록시를 만드는 것은 어떠한 문제를 해결할 수 있는가?
- 동적 프록시의 동작 순서는?
- JDK 동적 프록시의 한계는?
- CGLIB이란?
- CGLIB의 동작 순서는?
- CGLIB의 제약은?
- 인터페이스 유무에 따라 JDK 동적 프록시와 CGLIB을 사용하는 단점을 보완하는 방법은?
JDK 동적 프록시
- 기존 단순 프록시를 적용할 때는 적용 대상만큼 프록시 클래스를 만들어야 했음
- 적용 대상이 100개면 프록시를 100개 만들어야 함
- 코드는 거의 같은데 대상만 달라도 프록시를 새로 만들어야 하는 문제
- 동적 프록시는 개발자가 직접 프록시 클래스를 만들지 않아도 런타임에 자동으로 만들어 주는 기술
JDK 동적 프록시 구현 방법
public interface AInterface {
void call();
}
public class AImpl implements AInterface {
@Override
public void call() {
System.out.println("AImpl 호출");
}
}
public interface BInterface {
void call();
}
public class BImpl implements BInterface {
@Override
public void call() {
System.out.println("BImpl 호출");
}
}
- JDK 동적 프록시를 구현하려면 단순 구현체 클래스면 안되고 인터페이스가 있어야만 함
@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);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}ms", resultTime);
return result;
}
}
- invoke 함수에 원하는 코드를 구현
- method.invoke 부분이 실제 메서드 호출 부분이며 안에 target과 인자가 들어감
@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());
}
@Test
void dynamicB() {
BInterface target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
BInterface proxy = (BInterface) Proxy.newProxyInstance(
BInterface.class.getClassLoader(),
new Class[]{BInterface.class},
handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
- AInterface와 BInterface는 서로 다른 클래스인데 같은 프록시(TimeInvocationHandler)를 사용
- Proxy를 생성을 위한 인자만 직접 넣어주면 됨
JDK 동적 프록시 실행 순서
- 클라이언트는 JDK의 동적 프록시의 call() 실행
- JDK 동적 프록시는 InvocationHandler.invoke()를 호출하고 TimeInvocationHandler가 구현체이므로 TimeInvocationHandler.invoke()가 호출 됨
- TimeInvocationHandler가 내부 로직을 수행하고, method.invoke(target, args)를 호출해서 target인 실제 객체(AImpl)을 호출
- AImpl 인스턴스의 call()이 실행 됨
- AImpl 인스턴스의 call() 실행이 끝나면 TimeInvocationHandler로 응답이 돌아오고 시간 로그를 출력하고 결과를 반환
JDK 동적 프록시 적용 후 변경 점
- InvocationHandler가 Interface의 구현체 Proxy를 직접 만들어 줌
- 기존에는 각각 클래스를 만들어 줘야 했는데 InvocationHandler 하위에 하나만 만들어 주면 알아서 프록시 클래스를 생성해줌
JDK 동적 프록시를 로그트레이스 예제에 적용
@Slf4j
public class LogTraceBasicHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
public LogTraceBasicHandler(Object target, LogTrace logTrace) {
this.target = target;
this.logTrace = logTrace;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
status = logTrace.begin(message);
// 실제 메서드 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
if (status != null) {
logTrace.exception(status, e);
}
throw e;
}
}
}
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(
OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(orderController, logTrace)
);
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(
OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceBasicHandler(orderService, logTrace)
);
return proxy;
}
...
}
- InvocationHandler를 구현하는 LogTraceBasicHandler에 LogTrace 코드를 넣어 구현
- Config에서 빈을 리턴할 때 JDK 동적 프록시를 리턴
JDK 동적 프록시의 한계
- JDK 동적 프록시는 인터페이스가 한계
- 인터페이스가 없이 동적 프록시를 적용하려면 CGLIB이라는 바이트 코드 조작 특별 라이브러리 사용해야 함
CGLIB
- CGLIB: Code Generator Library
- 바이트 코드를 조작해 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리
- 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들 수 있음
- 원래는 외부 라이브러리인데, 스프링이 내부 소스에 포함시켜서 별도 라이브러리 추가 하지 않아도 사용 가능
- JDK 동적 프록시에서는 InvocationHandler를 사용했는데 CGLIB에서는 MethodInterceptor를 사용
CGLIB 사용 방법
@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 proxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// 실제 메서드 호출
Object result = proxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}ms", resultTime);
return result;
}
}
- 사용 방법이 JDK 동적 프록시와 거의 똑같음
CGLIB의 동작 순서
- CGLIB을 사용하면 실제 구현 클래스인 ConcreteService를 상속하는 가짜 동적 프록시 클래스를 생성
- 따라서, Client는 ConcreteService를 상속하는 가짜 프록시 클래스를 호출
- 가짜 프록시 클래스는 MethodInterceptor를 호출
- MethodInterceptor는 우리가 원하는 코드를 호출한 이후에 실제 타겟을 호출
CGLIB의 제약
- 인터페이스가 아닌 클래스 기반 상속을 사용하는 방법은 몇 가지 제약이 있음
- 부모 클래스의 생성자를 체크해야 함 -> CGLIB은 자식 클래스를 동적으로 생성하기 때문에 기본 생성자 필요
- 클래스에 final 키워드가 붙으면 상속이 불가능
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없음
JDK 동적 프록시와 CGLIB을 사용하는 것의 문제
- 인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고 인터페이스가 없는 경우에는 CGLIB을 적용해야 함
- 그러면 같은 코드를 사용하는데도 두 번의 작업을 해야 하는 문제가 생김
- 이러한 문제를 뒤에 프록시 팩토리해서 해결 가능