지난 포스팅에서 프록시를 이용해 기존 코드의 변경 없이 부가기능을 추가하는 방법에 대해서 알아보았다.
하지만 문제가 있었는데, 기능 적용 대상 클래스의 개수만큼 프록시 클래스를 정의해줘야 한다는 것이다.
결국 동일한 코드의 중복이 그만큼 늘어나게 된다.
이러한 문제를 해결할 수 있는 방법이 바로 동적 프록시이다.
이번 포스팅에서는 동적 프록시에 대해 알아보자.
동적 프록시에 대해 이해하기 위해 먼저 리플렉션에 대해 간단하게 알아보자.
리플렉션은 객체의 메타 정보를 획득하여 호출하는 메서드를 동적으로 변경할 수 있다.
코드로 바로 확인해보자.
@Test
void reflection2() throws Exception {
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start");
Object result = method.invoke(target);
log.info("result={}", result);
}
@Slf4j
static class Hello {
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
여기서 주목할 것은 메서드를 직접 호출하는 것이 아닌 Method로 호출한다는 것이다. 메서드 이름을 직접 명시하지 않기 때문에 이제 공통 로직을 만들 수 있다.
동적 프록시는 리플렉션을 활용해 런타임에 동적으로 프록시 객체를 개발자 대신 생성해준다. 또한 프록시에 실행 로직을 지정할 수 있다.
JDK동적 프록시는 인터페이스를 기반으로 프록시를 생성해주므로 인터페이스가 필수다.
코드로 확인해보자.
public interface AInterface {
String call();
}
public interface BInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface{
@Override
public String call() {
log.info("A 호출");
return "A";
}
}
@Slf4j
public class BImpl implements BInterface{
@Override
public String call() {
log.info("B 호출");
return "B";
}
}
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
@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={}",resultTime);
return result;
}
}
@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());
}
그럼 이제 로그 추적기에 동적 프록시를 적용해보자.
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;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 메서드 이름 필터
String methodName = method.getName();
// patterns와 methosName이 매칭되지 않는 경우 타겟 바로 실행
if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
return method.invoke(target, args);
}
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) {
logTrace.exception(status, e);
throw e;
}
}
}
이렇게 하면 동적 프록시 적용을 위한 준비는 끝났다. 프록시 적용을 위해 타겟마다 프록시 클래스를 정의해줬던 것과 달리 하나의 동적 프록시 클래스만 정의해주면 끝난다.
이제 동적 프록시를 사용하도록 설정해주자.
@Configuration
public class DynamicProxyFilterConfig {
private static final String[] PATTERNS = {"request*", "order*", "save*"};
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderControllerV1 = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceFilterHandler(orderControllerV1, logTrace, PATTERNS));
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 orderServiceV1 = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceFilterHandler(orderServiceV1, logTrace, PATTERNS));
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1Impl orderRepository = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceFilterHandler(orderRepository, logTrace, PATTERNS));
return proxy;
}
}
JDK 동적 프록시는 인터페이스와 리플렉션을 활용해 동적으로 프록시를 생성해준다.
동적 프록시 클래스를 생성하는 방법은 InvocationHanlder 인터페이스를 구현하면 되는데, invoke메서드를 오버라이드하여 부가 로직을 수행하고, 타겟의 로직을 수행할 때는 method.invoke(타겟 객체, args)를 호출하면 된다.
동적 프록시를 빈으로 등록하기 위해서는 Proxy객체를 활용하고,
Proxy.getInstance(클래스 로더, 인터페이스, InvocationHandler구현체)를 호출하면 된다.
여기까지 JDK동적 프록시를 활용해 프록시를 여러개 정의할 필요 없이 동적으로 생성하는 방법에 대해서 알아보았다.
하지만 만약 현재 진행하고 있는 프로젝트는 인터페이스를 사용하지 않고 구체 클래스만 사용할 경우 프록시를 어떻게 적용해야 할까?
다음 포스팅에서 알아보자.
출처 : 김영한 - 스프링 핵심 원리 고급편