[Spring] 동적 프록시 기술
JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 기술을 활용하면 프록시 객체를 동적으로 만들 수 있다.
이전처럼 프록시 객체를 계속해서 만들지 않고 하나만 만들고 동적 프록시로 적용해보자.
자바의 리플렉션 기술을 사용하면 클래스나 메서드의 메타데이터를 동적으로 획득하고 코드도 호출할 수 있다.
@Slf4j
static class Hello {
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
@Test
void reflection1() throws Exception {
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
Method methodCallA = classHello.getMethod("callA");
Object result1 = methodCallA.invoke(target);
log.info("result1={}", result1);
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result2={}", result2);
}
@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);
}
클래스 메타정보를 획득할 때 내부클래스 정보를 가져오는 경우 $를 사용한다.
Method 객체를 사용해서 메서드를 따로 분리했는데, 이 부분을 통해 공통되는 부분을 분리한다.
invoke 메서드의 파라미터인 target은 실제 메서드를 가지고 있는 클래스이다.
정적인 코드를 리플렉션을 사용해 메타정보로 추상화했다. (Method)
이 메타정보를 통해 공통 로직을 분리해낼 수 있었지만... 리플렉션 기술은 런타임에 동작하기에 컴파일 시점에 발생하는 오류를 인식할 수 없다.
따라서 리플렉션 기술은 정말 일반적인 공통 처리가 필요한 경우에만 사용해야 한다.
JDK 동적 프록시는 자바가 기본적으로 제공하고, 인터페이스를 기반으로 프록시를 만들기에 인터페이스가 꼭 사용되어야 한다.
@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;
}
}
메서드의 실행 시간을 출력하는 기능을 공통으로 적용해보자.
InvocationHandler 인터페이스를 구현해야 JDK 동적 프록시에 적용할 공통 로직을 개발할 수 있다.
invoke 메서드의 파라미터로는 동적 프록시가 호출할 대상, 메서드를 실행할 때 넘겨줄 인수가 있다.
@Test
void dynamic() {
Interface1 target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
AInterface proxy = (AInterface) Proxy.newProxyInstance(Interface1.class.getClassLoader(), new Class[]{Interface1.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
public interface Interface1 {
String call();
}
@Slf4j
public class Impl1 implements Interface1 {
@Override
public String call() {
log.info("호출");
return "1";
}
}
Proxy 클래스를 사용해 동적 프록시를 생성한다.
클래스 로더 정보, 인터페이스, 핸들러 로직을 넣어주면 인터페이스를 기반으로 동적 프록시를 생성하고 결과를 반환한다.
클래스 로더 : 프록시 클래스를 로드하기 위해 사용된다. 특정 클래스를 JVM 메모리에 로드하는 역할을 수행한다.
인터페이스 배열 : 인터페이스가 여러 개면 배열에 담아준다. 프록시 클래스는 인터페이스의 모든 메서드를 구현한다.
핸들러 : 프록시 객체의 메서드가 호출될 때 실행되는 메서드이다.
반환 타입이 Object라서 캐스팅이 필요하다.
1. 클라이언트가 프록시의 메서드를 호출한다.
2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다.
3. 핸들러가 내부 로직을 수행하고 method.invoke 를 호출해 실제 객체를 호출한다.
이제 부가기능을 구현할 때 같은 코드를 여러번 반복해서 쓸 필요가 없어졌다.
JDK 동적 프록시 기술이 프록시를 동적으로 만들어 공통으로 적용해준다.
@Configuration
public class DynamicProxyBasicConfig {
@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 LogTraceBasicHandler(orderControllerV1, logTrace));
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 LogTraceBasicHandler(orderServiceV1, 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;
}
}
실제 서비스에 적용하는건 기존 프록시 패턴을 사용할 때와 비슷하다.
프록시 객체를 반환하도록 스프링 빈을 등록해주자.
특정 메서드에는 핸들러 로직이 실행되지 않도록 설정하려면 얻어온 메서드 명을 통해 걸러내면 된다. (PatternMatchUtils를 활용하자)
JDK 동적 프록시는 인터페이스가 있어야 적용할 수 있다.
인터페이스 없이 바로 클래스로 설계된 애플리케이션에 동적 프록시를 적용하려면 CGLIB를 사용하자.
Code Generator Library의 약자로 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술이다.
원래는 외부 라이브러리였지만 이제는 스프링 내부 코드에 포함되어있어 스프링을 사용하면 그냥 쓸 수 있다.
@Slf4j
public class TimeMethodInterceptor implements MethodInterceptor {
private final Object target;
public TimeMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
Method 대신 MethodProxy를 사용하면 원본 객체의 메서드에 더 빠르게 접근하고 리플렉션을 사용하지 않아도 된다.
target은 항상 그렇듯 프록시가 호출할 실제 대상이다.
@Test
void cglib() {
ConcreteService target = new ConcreteService();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(ConcreteService.class);
enhancer.setCallback(new TimeMethodInterceptor(target));
ConcreteService proxy = (ConcreteService) enhancer.create();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
proxy.call();
}
public void setCallback(final Callback callback) {
setCallbacks(new Callback[]{callback});
}
public interface MethodInterceptor extends Callback {
Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;
}
CGLIB는 Enhancer 클래스를 사용해서 프록시를 생성한다.
동적으로 생성된 서브 클래스를 통해 원본 클래스의 메서드 호출을 가로챈다.
setSuperClass() 메서드로 어떤 클래스를 상속받을지 지정해주고
setCallback() 메서드로 프록시에 적용할 실행 로직을 할당해준다. (Callback을 파라미터로 받는데 MethodInterceptor는 Callback을 상속받는다)
이후 create 메서드로 프록시를 생성한다.
이 때 생성되는 프록시는 이전에 지정한 클래스를 상속받은 클래스이다.
로직을 보면 알 수 있듯 CGLIB도 상속을 사용하기에 상속과 관련된 제약이 따라온다.
자식 클래스를 동적으로 생성하기에 부모 클래스를 정의할 때 기본 생성자가 필요하고 클래스나 메서드에 final 키워드가 붙어있으면 상속이 불가능하고 오버라이드 할 수 없다는 점을 유의하자.
'Spring > Spring' 카테고리의 다른 글
[Spring] 빈 후처리기 (0) | 2023.07.26 |
---|---|
[Spring] 프록시 팩토리 (0) | 2023.07.24 |
[Spring] 프록시 패턴과 데코레이터 패턴 (0) | 2023.07.22 |
[Spring] 템플릿 메서드 패턴과 전략 패턴 (0) | 2023.07.19 |
[Spring] 로그 추적기와 쓰레드 로컬 (0) | 2023.07.16 |
댓글
이 글 공유하기
다른 글
-
[Spring] 빈 후처리기
[Spring] 빈 후처리기
2023.07.26 -
[Spring] 프록시 팩토리
[Spring] 프록시 팩토리
2023.07.24 -
[Spring] 프록시 패턴과 데코레이터 패턴
[Spring] 프록시 패턴과 데코레이터 패턴
2023.07.22 -
[Spring] 템플릿 메서드 패턴과 전략 패턴
[Spring] 템플릿 메서드 패턴과 전략 패턴
2023.07.19