개요
- 프록시를 사용해서 기존의 코드를 건드리지 않고, 새로운 기능을 추가할 수 있다.
- 하지만 프록시를 적용하고 싶은 대상의 숫자 만큼 프록시 클래스를 만들어야 한다는 단점이 있다.
- 이러한 문제를 해결하는 방법이 바로 동적 프록시이다.
- 그리고 동적프록시를 만드는 방법중 하나인 JDK 동적 프록시를 구현하는 방법을 알아보려고 한다.
코드 예시
- 먼저 아래와 같이 2개의 인터페이스와 구현체를 구현한다.
- 프록시를 적용하고 싶은 대상이 된다.
public interface AInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
public interface BInterface {
String call();
}
@Slf4j
public class BImpl implements BInterface {
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
- JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.
@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;
}
}
- invoke 메소드의 인자는 아래와 같다.
- proxy: 프록시 자신
- method: 호출한 메서드
- args: 메소드를 호출할 때 전달한 인수
- 먼저 동적 프록시가 호출할 대상인 target을 인자로 가지고 있다.
- 그리고 method.invoke(target, args); -> 리플렉션을 사용해서 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();
}
@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();
}
}
- new TimeInvocationHandler(target);: 동적 프록시에 적용할 핸들러 로직이다.
- newProxyInstance: 이 메소드를 사용해서 동적 프록시를 생성한다.
- 인자로 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다.
- 그렇게 되면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
애플리케이션에 적용
- 아래와 같이 애플리케이션이 구현되어 있다고 가정하자
- 여기서 OrderController과 OrderService에 동적 프록시를 적용한다면 아래와 같은 구조가 될 것이다.
- 런타임 시에는 아래와 같이 의존 관계가 형성된다.
코드 구현
- 위와 마찬가지로 동적 프록시에 적용할 로직을 구현하기 위해서 InvocationHandler를 구현해서 LogTraceBasicHandler를 아래와 같이 구현한다.
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();
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;
}
}
}
- target: 프록시가 호출할 대상이다.
- PatternMatchUtils.simpleMatch를 이용해서 특정 메서드 이름이 매칭 되는 경우에만 LogTrace 로직을 실행하도록 구현한다.
- xxx : xxx가 정확히 매칭되면 참 xxx* : xxx로 시작하면 참
- *xxx : xxx로 끝나면 참
- xxx : xxx가 있으면 참
- 이름이 매칭되지 않으면 실제 로직을 바로 호출한다.
Config 설정
@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) {
OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceFilterHandler(orderRepository, logTrace, PATTERNS));
return proxy;
}
}
- public static final String[] PATTERNS = {"request*", "order*", "save*"}; : request , order , save 로 시작하는 메서드에 로그가 남기기 위해서 패턴을 정의한다.
- Controller , Service , Repository 에 맞는 동적 프록시를 생성해주면 된다.
- 동적 프록시를 만들더라도 LogTrace 를 출력하는 로직은 모두 같기 때문에 프록시는 모두 LogTraceBasicHandler 를 사용한다.
- Controller , Service , Repository 모두 proxy를 반환한다. -> 의존관계를 proxy로 설정한다.
- 대신에 proxy에서 실제 구현체를 target으로 가지고 있고, proxy 로직이 수행되는 중간에 실제 구현체를 실행한다.
References
- 김영한님의 스프링 고급편
'Spring' 카테고리의 다른 글
SpringMVC를 이용해서 요청 Body값 Trim처리하기 (0) | 2023.03.06 |
---|---|
AbstractRoutingDataSource를 통한 Multi-DataSource 구현 (0) | 2023.02.04 |
ApplicationEventPublisher (0) | 2022.06.11 |
MapStruct (0) | 2022.04.09 |
Handler Methods (0) | 2022.04.09 |