개발언어/JAVA
[Spring AOP] 1.JDK Dynamic Proxy/CGLIB로 인터페이스 or 클래스 기반의 프록시 객체를 쉽게 생성하는 방법
nomoreFt
2022. 11. 16. 22:55
JDK Dynamic Proxy
JDK Dynamic Proxy란?
- JDK에서 제공하는 Dynamic Proxy 기능을 사용하면 인터페이스 기반의 프록시 객체를 쉽게 생성할 수 있다.
- JDK Dynamic Proxy는 인터페이스를 구현한 클래스의 객체를 생성할 때 사용한다.(인터페이스가 필수)
- JDK Dynamic Proxy는
InvocationHandler
인터페이스를 구현한 클래스의 객체를 생성자 파라미터로 넘겨주면 된다. - JDK Dynamic Proxy는 인터페이스의 모든 메서드를 구현한 프록시 객체를 생성한다.
JDK Dynamic Proxy 예제
공통 기능을 가진 Proxy를 구현
@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 start = System.currentTimeMillis();
Object result = method.invoke(target, args);
long end = System.currentTimeMillis();
log.info("TimeProxy 종료");
log.info("TimeProxy 실행 시간: {}ms", end - start);
return result;
}
}
- InvocationHandler을 구현하여 공통 기능을 구현한다. (invoke 메서드)
JDK Dynamic Proxy를 사용해 구현
@Slf4j
public class JdkDynamicProxyTest {
/**
* 핵심은 target용 Proxy 객체를 각각 생성해준 것이 아닌,
* TimeInvocationHandler에 공통 기능을 담고, 공통기능을 A구현체, B구현체에 적용시킨 것이다.
*/
@Test
void dynamicA() {
//A 객체 앞에 TimeInvocationHanlder 적용
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("tagetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
//B 객체 앞에 TimeInvocationHanlder 적용
BInterface targetB = new BImpl();
TimeInvocationHandler handlerB = new TimeInvocationHandler(targetB);
BInterface proxyB = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader(),
new Class[]{BInterface.class},
handler);
proxyB.call();
log.info("tagetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
: Proxy 객체를 생성하는 메서드이다.
해당 메서드로 Proxy 객체를 받아 원하는 Interface로 형변환 하면 프록시 객체가 생성되는데,- 이 때, InvocationHandler의 invoke 메서드가 호출된다.(구현한)
JDK Dynamic Proxy 예제2
Method Naming 패턴에 따라 다른 로직을 수행하는 Proxy를 구현
공통 적용소스인 Handler를 제작한다. pattern에 따라 다른 로직을 수행한다. (pattern은 외부주입)
public class LogTraceFilterHandler implements InvocationHandler {
private final Object target;
private final LogTrace logTrace;
private final String[] patterns;
public LogTraceFilterHandler(Object target, LogTrace logTrace, String[] patterns) {
this.target = target;
this.logTrace = logTrace;
this.patterns = patterns;
}
/**
*
* method 이름 패턴에 따라 작동시킬지 결정
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//메서드 이름 필터
String methodName = method.getName();
/**
* 특정 메서드명 패턴에 매치되지 않으면 Log를 찍지 않고( 별다른 공통 소스 없이)
* 원본 메서드를 작동시킨다.
*/
//save, request, reque*, *est
if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
return method.invoke(target, args);
}
TraceStatus status = logTrace.begin(method.getDeclaringClass() + "." + methodName + "()");
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
}
}
Proxy를 사용하는 소스 Bean 등록 (Interface Impl, Handler, Proxy)
@Configuration
public class DynamicProxyFilterConfig {
/**
*메서드 명 패턴이 아래와 같아야 handler 실행
*/
/**
* [dc4b7a40] interface hello.proxy.app.v1.OrderControllerV1.request()
* [dc4b7a40] |-->interface hello.proxy.app.v1.OrderServiceV1.orderItem()
* [dc4b7a40] | |-->interface hello.proxy.app.v1.OrderRepositoryV1.save()
* [dc4b7a40] | |<--interface hello.proxy.app.v1.OrderRepositoryV1.save() time=1007ms
* [dc4b7a40] |<--interface hello.proxy.app.v1.OrderServiceV1.orderItem() time=1016ms
* [dc4b7a40] interface hello.proxy.app.v1.OrderControllerV1.request() time=1018ms
*/
private static final String[] PATTERNS = {"request", "order*", "save*"};
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = orderRepositoryV1(logTrace);
return (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceFilterHandler(new OrderControllerV1Impl(orderServiceV1(logTrace)), logTrace, PATTERNS));
}
@Bean
OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceFilterHandler(orderService, logTrace, PATTERNS));
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceFilterHandler(orderRepository, logTrace, PATTERNS));
return proxy;
}
}
JDK 동적 프록시를 적용한 뒤, 얻는 이점
- 공통 기능을 가진 Proxy 객체를 한번만 구현한 뒤, 원하는 모든 객체에 적용 가능하다.(필요할 때 생성해서)
CGLIB
Interface가 아닌 Class에도 Proxy를 적용할 수 있다.
바이트코드로 조작하는 방식이다. (JDK는 인터페이스 기반)
JDK Proxy vs CGLIB Proxy
- JDK Proxy는 Interface 기반으로 동작한다. (Interface가 없으면 동작하지 않는다.)
- CGLIB는 Interface가 없어도 동작한다. (Class 기반으로 동작한다.)
- CGLIB는 JDK Proxy보다 느리다. (JDK Proxy는 Interface 기반으로 동작하기 때문에, Interface가 있으면 바이트코드를 조작하지 않고 바로 호출한다.)
- CGLIB는 JDK Proxy보다 많은 메모리를 사용한다. (JDK Proxy는 Interface 기반으로 동작하기 때문에, Interface가 있으면 바이트코드를 조작하지 않고 바로 호출한다.)
- 네이밍 규칙 :
proxyClass=class com.sun.proxy.$Proxy0
vs대상클래스$$EnhancerByCGLIB$$임의코드
CGLIB는 보통 직접 구현해서 사용하는 경우는 없다. ProxyFactoryBean
을 사용한다.