JDK 동적 프록시
JDK 동적 프록시를 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어 준다. 그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 할당해주기 때문에 인터페이스가 필수이다.
자바가 기본으로 제공하는 JDK 동적 프록시를 알아보자.
A, BInterface를 만들고 이를 구현하는 아주 간단한 코드를 보자.
A인터페이스와 그 구현체
public interface AInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface{
@Override
public String call(){
log.info("A 호출");
return "a";
}
}
B인터페이스와 그 구현체
public interface BInterface {
String call();
}
@Slf4j
public class BImpl implements BInterface{
@Override
public String call(){
log.info("B 호출");
return "b";
}
}
JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현하여 작성하면 된다.
자바가 제공하는 InvocationHandler 인터페이스는 아래와 같이 생겼다.
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
이를 Implements하는 TimeInvocationHandler 요 클래스 안에 프록시의 부가 기능(해당 예제에서는 시간 측정로직)이 담긴다.
//프록시의 로직이 여기에 있음
@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("Time Proxy 실행");
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);//인자로 넘어온 target의 메서드 실행
long endTime = System.currentTimeMillis();
log.info("TimeProxy 종료 resultTime = {}", endTime-startTime);
return result;
}
}
이제 모든 준비가 끝났다. 이를 테스트해보자.
@Test
void dynamicA(){
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
//자바가 동적으로 프록시 객체 생성 //JDK 동적 프록시는 인터페이스를 기반으로 동작한다.
AInterface proxy = (AInterface) Proxy.newProxyInstance(
AInterface.class.getClassLoader(),
new Class[]{AInterface.class},
handler);
proxy.call();//handler의 invoke를 실행한다. 이때 invoke의 Method 타입 인자로 call이 들어옴
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
}
실행결과:
Time Proxy 실행
A 호출
TimeProxy 종료 resultTime = 1
targetClass = class hello.proxy.jdkdynamic.code.AImpl
proxyClass = class com.sun.proxy.$Proxy12 //여기에 $Proxy인 것을 보니 자바가 동적으로 생성해줌
BInterface에 대해 테스트하려면 위의 코드에서 A를 B로만 바꿔주면 된다.
@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();//handler의 invoke를 실행한다. 이때 invoke으 Method 인자로 call이 들어옴
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
}
실행결과:
Time Proxy 실행
B 호출
TimeProxy 종료 resultTime = 0
targetClass = class hello.proxy.jdkdynamic.code.BImpl
proxyClass = class com.sun.proxy.$Proxy13
실행 순서를 그림으로 나타내면 아래와 같다.
이제 JDK 동적 프록시 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다. 즉 같은 부가 기능 로직을 한번만 개발하여 공통으로 적용 가능하다. 이제 적용 대상 만큼 프록시를 만들어야 했던 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 SRP를 지킬 수 있게 되었다.
그림으로 JDK 동적 프록시를 적용하기 전과 후의 차이를 보자.
이제 지금까지 학습한 JDK 동적 프록시를 V1 App에 적용해보자.
V1 App에 동적 프록시 적용하기
먼저 로그 추적기 로직을 담은 InvocationHandler를 만들자.
import hello.proxy.trace.TraceStatus;
import hello.proxy.trace.logtrace.LogTrace;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
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);
//target 로직 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
}catch(Exception e){
logTrace.exception(status, e);
throw e;
}
}
}
이제 위의 LogTraceBasicHandler를 사용하여 의존성을 주입해보자.
import hello.proxy.app.v1.*;
import hello.proxy.trace.logtrace.LogTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Proxy;
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace){
OrderControllerV1Impl 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) {
OrderServiceV1Impl orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceBasicHandler(orderService, logTrace));
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 LogTraceBasicHandler(orderRepository, logTrace));
return proxy;
}
}
모두 Proxy를 반환하고 있다. 의존관계를 그림으로 보면 아래와 같다.
런타임 시에는 아래와 같은 순서로 수행된다.
마지막으로 아래의 @Import 설정을 해주는 것도 잊지 말자.
@Import(DynamicProxyBasicConfig.class)
//@Import(ConcreteProxyConfig.class)
//@Import(InterfaceProxyConfig.class)
//@Import({AppV1Config.class, AppV2Config.class})
//@Import(AppV1Config.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace(){
return new ThreadLocalLogTrace();
}
}
그런데 현재는 OrderControllerV1, OrderServiceV1, OrderRepositoryV1 모두가 프록시 객체에 의해 호출되기 전에 OrderControllerV1의 no-log메서드도 로그가 출력된다.
public class OrderControllerV1Impl implements OrderControllerV1{
private final OrderServiceV1 orderService;
public OrderControllerV1Impl(OrderServiceV1 orderService) {
this.orderService = orderService;
}
@Override
public String request(String itemId) {//생략}
// 얘도 로그가 출력된다.
@Override
public String noLog() {
return "ok";
}
}
이제 이 문제를 해결해보자.
이를 위해서는 메서드 이름에 따라 필터링을 할 수 있어야 한다.
아래와 같이 필터링을 추가한 InvocationHandler를 만들었다.
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;
}
}
}
그리고 Config에서 Pattern을 문자열 배열로 만들어서 인자로 넣어준다.
@Configuration
public class DynamicProxyFilterConfig {
//Patterns
private static final String[] PATTERNS = {"request*", "order*", "save*"};
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace){
OrderControllerV1Impl orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceFilterHandler(orderController, logTrace, PATTERNS));
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1Impl 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 동적 프록시를 적용하여 인터페이스가 있는 주문 서비스에 로그 추적기 로직을 첨가할 수 있었다. 하지만 JDK 동적 프록시를 사용한 방법은 반드시 인터페이스가 존재해야만 했다.
인터페이스가 없는 경우에는 CGLIB이라는 바이트 코드 조작 라이브러리를 사용하여 가능한데, 이는 다음 포스트에서 알아보자.
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
'Spring > spring AOP' 카테고리의 다른 글
스프링 코어 2 - 스프링이 지원하는 프록시(프록시 팩토리) (0) | 2022.01.10 |
---|---|
스프링 코어2 - CGLIB (0) | 2022.01.10 |
스프링 코어2 - 리플렉션 (0) | 2022.01.09 |
스프링 코어2 - 구체 클래스 기반 프록시 (0) | 2022.01.08 |
스프링 코어2 - 인터페이스 기반 프록시 (0) | 2022.01.08 |