참고자료
JDK 동적 프록시 이전 문제점
이 전 포스팅에서 프록시 패턴을 공부했는데, 문제가 여전히 있었다. 문제는 프록시 클래스를 일일이 다 하나씩 만들어 줘야 하는것. 다시 말해 프록시로 만들어 줄 클래스가 100개면 프록시 클래스도 100개가 있어야 한다는 것. 이 문제를 해결하기 위해 동적 프록시를 만들어서 단 하나의 프록시 클래스로 여러개의 클래스를 프록시화 할 수 있다.
JDK 동적 프록시
JDK 동적 프록시는 내부적으로 리플렉션 기술을 사용한다. 그리고 JDK 동적 프록시는 한가지 제약이 있다. 반드시 인터페이스가 있어야 한다. 반드시. 그래서 인터페이스를 두 개 만들고 그 인터페이스를 구현한 구현 클래스 두 개, 동적 프록시 클래스 한 개를 만들어 보겠다.
AInterface
package cwchoiit.springadvanced.proxy.jdkdynamic.code;
@FunctionalInterface
public interface AInterface {
String call();
}
BInterface
package cwchoiit.springadvanced.proxy.jdkdynamic.code;
@FunctionalInterface
public interface BInterface {
String call();
}
AImpl
package cwchoiit.springadvanced.proxy.jdkdynamic.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AImpl implements AInterface {
@Override
public String call() {
log.info("A 호출");
return "A";
}
}
BImpl
package cwchoiit.springadvanced.proxy.jdkdynamic.code;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class BImpl implements BInterface {
@Override
public String call() {
log.info("B 호출");
return "B";
}
}
- 아주 간단하게 AInterface, BInterface를 만들고 그것들을 구현한 AImpl, BImpl 클래스를 만들었다. 이제 동적 프록시를 만들어 보자.
JDK 동적 프록시 - 예제 코드
JDK 동적 프록시를 만드려면 java.lang.reflect.InvocationHandler를 구현해야 한다. 코드를 통해 바로 알아보자.
TimeInvocationHandler
package cwchoiit.springadvanced.proxy.jdkdynamic.code;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@Slf4j
@RequiredArgsConstructor
public class TimeInvocationHandler implements InvocationHandler {
private final Object 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();
log.info("TimeProxy 종료 {} ms", endTime - startTime);
return result;
}
}
- 구현할 메서드는 하나, invoke(Object proxy, Method method, Object[] args)이다. 여기서 proxy는 프록시 자신을 가리키고, method는 호출한 메서드를 말한다. args는 메서드를 호출할 때 전달한 인수들을 담고 있다. 없을수도 있다.
- TimeInvocationHandler는 InvocationHandler라는 인터페이스를 구현하고 이렇게 하면 JDK 동적 프록시에 적용할 공통 로직을 개발할 수 있다.
- 이렇게 동적 프록시 하나를 만들어 두면 어떻게 뭘 프록시를 만들 수 있을까?
JdkDynamicProxyTest
package cwchoiit.springadvanced.proxy.jdkdynamic;
import cwchoiit.springadvanced.proxy.jdkdynamic.code.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;
@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());
}
}
- new TimeInvocationHandler(target) → 동적 프록시에 적용할 핸들러 로직이다. 모든 프록시는 결국 실제 객체가 필요하기 때문에 실제 객체인 target을 받아야 한다.
- Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler) → 동적 프록시는 java.lang.reflect.Proxy를 통해서 생성할 수 있다. 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
- 원래 반환하는 반환타입은 Object이지만, 인터페이스를 사용한 프록시도 역시나 인터페이스를 구현한 구현체 중 하나이므로 이 반환된 프록시를 형변환해서 사용할 수 있다.
- proxy.call()을 호출하면, 기존 기능에 프록시 기능이 추가된 모습을 확인할 수 있다. 참고로 call() 메서드는 AInterface, BInterface가 가지고 있는 메서드이다.
실행 결과
18:32:50.497 [Test worker] INFO cwchoiit.springadvanced.proxy.jdkdynamic.code.TimeInvocationHandler -- TimeProxy 실행
18:32:50.499 [Test worker] INFO cwchoiit.springadvanced.proxy.jdkdynamic.code.AImpl -- A 호출
18:32:50.499 [Test worker] INFO cwchoiit.springadvanced.proxy.jdkdynamic.code.TimeInvocationHandler -- TimeProxy 종료 1 ms
18:32:50.499 [Test worker] INFO cwchoiit.springadvanced.proxy.jdkdynamic.JdkDynamicProxyTest -- targetClass = class cwchoiit.springadvanced.proxy.jdkdynamic.code.AImpl
18:32:50.499 [Test worker] INFO cwchoiit.springadvanced.proxy.jdkdynamic.JdkDynamicProxyTest -- proxyClass = class jdk.proxy3.$Proxy12
생성된 JDK 동적 프록시
proxyClass = class jdk.proxy3.$Proxy12 이 부분이 동적으로 생성된 프록시 클래스 정보이다. 이것은 우리가 직접 만든 클래스가 아니라 JDK 동적 프록시가 이름 그대로 동적으로 만들어준 프록시이다.
실행 순서
- 클라이언트는 JDK 동적 프록시의 call()을 실행한다.
- JDK 동적 프록시는 InvocationHandler.invoke()를 호출한다. TimeInvocationHandler가 구현체로 있으므로 TimeInvocationHandler.invoke()가 호출된다. 이때 파라미터로 받는 Method method는 클라이언트가 호출한 call() 메서드이다.
- TimeInvocationHandler가 내부 로직을 수행하고, method.invoke(target, args)를 호출해서 target인 실제 객체(AImpl) 호출한다.
- AImpl 인스턴스의 call()이 실행된다.
- AImpl 인스턴스의 call()의 실행이 끝나면 TimeInvocationHandler로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
실행 순서 그림
- 그림으로 보면 알겠지만, JDK 동적 프록시가 하는 일은 InvocationHandler의 invoke()를 호출하는 일만 한다. invoke(Object proxy, Method method, Object[] args)를 호출할 때 전달해주는 method는 클라이언트가 호출한 메서드인 call()과 call()을 호출할 때 만약 파라미터가 있었다면 그 값을 args로 전달한다.
정리
예제를 보면, AImpl, BImpl의 프록시를 만든적이 없다. 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler는 공통으로 사용했다. JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다. 그리고 같은 부가 기능 로직을 한번만 개발해서 공통으로 적용할 수 있다. 만약 적용 대상이 100개여도 동적 프록시를 통해서 생성하고, 각각 필요한 InvocationHandler만 만들어서 넣어주면 된다.
결과적으로, 프록시 클래스를 클래스 수만큼 만들어야 하는 문제도 해결하고, 부가 기능 로직도 하나의 클래스에 모아서 단일 책임 원칙(SRP)도 지킬수 있게 됐다.
- 최종 그림이다. 점선은 개발자가 직접 만든 클래스가 아니다.
JDK 동적 프록시 - 적용1
마찬가지로 InvocationHandler를 구현하는 구현체 하나를 만들면 된다.
LogTraceBasicHandler
package cwchoiit.springadvanced.proxy.config.dynamicproxy.handler;
import cwchoiit.springadvanced.trace.TraceStatus;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class LogTraceBasicHandler implements InvocationHandler {
private Object target;
private LogTrace trace;
public LogTraceBasicHandler(Object target, LogTrace trace) {
this.target = target;
this.trace = trace;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
TraceStatus status = null;
try {
String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
status = trace.begin(message);
Object result = method.invoke(target, args);
trace.end(status);
return result;
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
}
- 필요한 실제 객체와 LogTrace 객체를 주입받는다. 그 외 내용은 전부 이전과 동일하다.
- 그런데, 그럼 메시지는 어떻게 남길까? "어떤 클래스의 어떤 메서드가 호출됐는가?"에 대한 메시지를 이전에는 문자로 입력했는데 이젠 그렇게 할 수가 없다. 동적 프록시로 만들때 이 LogTraceBasicHandler는 딱 하나니까. 그 해결법은 위 코드처럼 실행된 메서드를 통해서 구할 수 있다. 실행된 메서드에는 자기의 클래스 정보와 자기 자신의 이름 정보가 다 들어있다.
- 그리고 이제 이 핸들러를 이용해서 빈으로 프록시를 등록하자.
DynamicProxyBasicConfig
package cwchoiit.springadvanced.proxy.config.dynamicproxy;
import cwchoiit.springadvanced.proxy.app.v1.*;
import cwchoiit.springadvanced.proxy.config.dynamicproxy.handler.LogTraceBasicHandler;
import cwchoiit.springadvanced.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 OrderRepositoryV1 orderRepositoryV1(LogTrace trace) {
OrderRepositoryV1Impl target = new OrderRepositoryV1Impl();
return (OrderRepositoryV1) Proxy.newProxyInstance(
OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceBasicHandler(target, trace));
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace trace) {
OrderServiceV1Impl target = new OrderServiceV1Impl(orderRepositoryV1(trace));
return (OrderServiceV1) Proxy.newProxyInstance(
OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceBasicHandler(target, trace));
}
@Bean
public OrderControllerV1 orderControllerV1(LogTrace trace) {
OrderControllerV1Impl target = new OrderControllerV1Impl(orderServiceV1(trace));
return (OrderControllerV1) Proxy.newProxyInstance(
OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(target, trace));
}
}
- 프록시를 만들어내는 코드 역시 위와 동일하다. 이렇게 실제 코드에도 적용해보았다. 이 동적 프록시의 단점이라고 한다면 인터페이스가 반드시 존재해야 한다는 것이다. 구체 클래스에는 적용이 불가하다.
- 이제 이렇게 만든 동적 프록시로 실제 클라이언트가 요청을 보내보자. 요청을 보내면 요청 시간에 대한 시간이 로그로 남아야 한다.
- 그 전에 이 과정을 그림으로 보면 다음과 같다.
실행 결과
[5c7aea8c] OrderControllerV1.request()
[5c7aea8c] |--->OrderServiceV1.orderItem()
[5c7aea8c] | |--->OrderRepositoryV1.save()
[5c7aea8c] | |<---OrderRepositoryV1.save() time=1001ms
[5c7aea8c] |<---OrderServiceV1.orderItem() time=1002ms
[5c7aea8c] OrderControllerV1.request() time=1002ms
- 원하던대로 소요 시간에 대한 로그가 잘 남았다. 하지만 문제가 있다. 어떤 문제냐면, /v1/no-log로 요청했을 땐 로그가 남지 않길 원하는 경우에도 로그가 남는다. 왜냐하면 우리의 동적 프록시에는 no-log를 걸러내는 부분이 없기 때문이다. 이를 간단히 해결할 수 있다. 필터링을 사용해서 말이다.
JDK 동적 프록시 - 적용2
위에서 말한 문제인, 로그가 남으면 안되는 메서드까지도 로그가 남았다. 그 이유는 실제 객체를 넘겨주고 프록시로 만들면 프록시는 어떤 메서드가 호출되든지 InvocationHandler의 invoke()가 호출되기 때문이다. 이 문제를 간단하게 필터링을 통해 해결해보자.
LogTraceFilterHandler
package cwchoiit.springadvanced.proxy.config.dynamicproxy.handler;
import cwchoiit.springadvanced.trace.TraceStatus;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import org.springframework.util.PatternMatchUtils;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class LogTraceFilterHandler implements InvocationHandler {
private Object target;
private LogTrace trace;
private final String[] patterns;
public LogTraceFilterHandler(Object target, LogTrace trace, String[] patterns) {
this.target = target;
this.trace = trace;
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() + "." + methodName + "()";
status = trace.begin(message);
Object result = method.invoke(target, args);
trace.end(status);
return result;
} catch (Exception e) {
trace.exception(status, e);
throw e;
}
}
}
- 이 필터 기능이 추가된 동적 프록시는 생성자에서 파라미터로 patterns를 추가적으로 받는다. 이 패턴에 부합하는 요청만 로그기능을 추가해주고 부합하지 않는 경우엔 그냥 바로 실제 객체의 결과만을 리턴한다. 그 부분이 다음 부분이다.
String methodName = method.getName();
if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
return method.invoke(target, args);
}
- invoke()의 파라미터로 넘어오는 method의 이름을 받는다. 그 이름이 patterns에 일치하지 않으면 로그 기능은 추가하지 않는 코드이다. PatternMatchUtils는 스프링에서 제공해주는 유틸리티성 클래스이다.
- 이제 이 필터 기능이 있는 동적 프록시로 다시 빈을 등록해보자.
DynamicProxyFilterConfig
package cwchoiit.springadvanced.proxy.config.dynamicproxy;
import cwchoiit.springadvanced.proxy.app.v1.*;
import cwchoiit.springadvanced.proxy.config.dynamicproxy.handler.LogTraceBasicHandler;
import cwchoiit.springadvanced.proxy.config.dynamicproxy.handler.LogTraceFilterHandler;
import cwchoiit.springadvanced.trace.logtrace.LogTrace;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Proxy;
@Configuration
public class DynamicProxyFilterConfig {
private static final String[] PATTERNS = {"request*", "order*", "save*"};
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace trace) {
OrderRepositoryV1Impl target = new OrderRepositoryV1Impl();
return (OrderRepositoryV1) Proxy.newProxyInstance(
OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class},
new LogTraceFilterHandler(target, trace, PATTERNS));
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace trace) {
OrderServiceV1Impl target = new OrderServiceV1Impl(orderRepositoryV1(trace));
return (OrderServiceV1) Proxy.newProxyInstance(
OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class},
new LogTraceFilterHandler(target, trace, PATTERNS));
}
@Bean
public OrderControllerV1 orderControllerV1(LogTrace trace) {
OrderControllerV1Impl target = new OrderControllerV1Impl(orderServiceV1(trace));
return (OrderControllerV1) Proxy.newProxyInstance(
OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class},
new LogTraceFilterHandler(target, trace, PATTERNS));
}
}
- 패턴이 추가됐다. no-log는 패턴에 들어가지 않는다. 이렇게 동적 프록시를 빈으로 등록하면 이제 /v1/no-log 로 요청해도 로그는 남지 않는다.
결론
JDK 동적 프록시를 이용해서 프록시를 일일이 다 만드는게 아니라 프록시가 처리해야 하는 공통 기능 코드를 InvocationHandler 하나만 만들어서 필요한 클래스마다 JDK 동적 프록시를 통해 프록시를 입혀봤다. 이 JDK 동적 프록시는 인터페이스가 필수이기 때문에 인터페이스가 없으면 안된다는 제약이 있다. 그래서 구체 클래스에는 적용하지 못한다는 단점이 있다. 이를 해결하기 위해서는 CGLIB라는 기술을 사용하면 된다.
'Spring Advanced' 카테고리의 다른 글
빈 후처리기(BeanPostProcessor) (2) | 2023.12.27 |
---|---|
Advisor, Advice, Pointcut (0) | 2023.12.15 |
Proxy/Decorator Pattern (0) | 2023.12.13 |
Template Callback Pattern (0) | 2023.12.12 |
Strategy Pattern (2) | 2023.12.12 |