리플렉션
void reflection0() {
Hello target = new Hello();
//공통 로직1 시작
log.info("start");
String result1 = target.callA(); //호출하는 메서드가 다음
log.info("result={}", result1);
//공통 로직1 종료
//공통 로직2 시작
log.info("start");
String result2 = target.callB(); //호출하는 메서드가 다음
log.info("result={}", result2);
//공통 로직2 종료
}
거의 로직1 과 로직 2가 유사하지만 공통 메소드로 묶기가 힘들다. 중간에 호출하는 메서드가 다르기 때문이다.
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);
}
리플렉션을 통해 class의 metadata를 얻어오고 기존의 callA(), callB() 가 Method로 대체가 되었다.
공통로직으로 뽑아 낼수 있게 된것이다.
하지만 런타임에 동작하기에 컴파일타임에 오류를 잡을수 없다.
JDK 동적 프록시
인터페이스가 있어야만 프록시를 만들 수 있다.
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;
}
}
어떤 method가 호출될지 넘어오게 된다. 그것을 그냥 invoke로 호출하고 그 결과를 어떻게 추가적으로 처리할 것인지를 작성하면 된다.
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());
}
handler가 위에서 정의한 일종의 데코레이터 느낌으로 newProxyInstance인자로 넣어준다. 그러면 해당 feature가 적용된 상태의 결과 값을 얻을 수 있게 된다.
proxy.call 을 하게되면 위에 handler에게 call method가 전달되게 된다. handler는 실제 구현체인 AImpl을 target으로 가지고 있으니 해당 구현체의 call 을 호출하게 된다.
JDK 동적프록시를 사용하면 적용 대상만큼 프록시 객체를 내가 일일이 만들 필요가 없다.
또한 적용하려는 로직을 InvocationHandler로 만들고 프록시를 만들때 넣어주면 된다.
Controller에 적용해보자
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);
//로직 호출
Object result = method.invoke(target, args);
logTrace.end(status);
return result;
} catch (Exception e) {
logTrace.exception(status, e);
throw e;
}
}
}
@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;
}
@Configuration 에서 spring으로 하여금 controller proxy bean을 관리하게 만든다.
어떤 클래스로더로 불러 올것인가?
어떤 인터페이스를 기반으로 프록시 클래스를 만들것인가?
만든 프록시 클래스에 어떤 로직을 더 추가적으로 적용하고 실제 로직은 어디서 호출하는가?
CGLIB
부모 클래스의 생성자를 체크 해야함 -> CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본생성자가 필요하다.
클래스나 메서드에 final 이 붙으면 상속 및 오버라이딩 할 수 없다. -> CGLIB에서 예외발생하거나 , 프록시로직이 동작하지 않음
JDK 동적 프록시의 InvocationHandler 처럼 CGLIB 는 MethodInterceptor를 구현해서 로직을 추가하면 된다.
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;
}
}
JDK동적프록시와 동일하게 method.invoke(target,args)를 해도 되지만 methodProxy를 통해 호출하는 것이 내부적으로 최적화 된 기술을 사용할 수 있다.
@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();
}
ConcreteService는 클래스이다. 인터페이스가 아니다.
해당 클래스를 상속받은 프록시를 만들기위해 setSuperClass 에 구체클래스를 지정해준다.
'WEB > Spring' 카테고리의 다른 글
스프링 핵심 원리 - 고급편 6) 빈 후처리기 (0) | 2022.06.09 |
---|---|
스프링 핵심 원리 - 고급편 5) 스프링이 지원하는 프록시 (0) | 2022.06.09 |
스프링 핵심 원리 - 고급편 4) 프록시 패턴과 데코레이터 패턴 (0) | 2022.06.06 |
스프링 핵심원리 고급편 3) 템플릿 메서드 패턴과 콜백 패턴 (0) | 2022.06.06 |
스프링 핵심원리 고급편 2) ThreadLocal (0) | 2022.06.05 |