리플렉션
→ 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
ReflectionTest
@Slf4j
public class ReflectionTest {
@Test
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 종료
}
@Slf4j
static class Hello {
public String callA() {
log.info("callA");
return "A";
}
public String callB() {
log.info("callB");
return "B";
}
}
}
→ 공통 로직1과 공통 로직 2를 하나의 메서드로 뽑아서 합칠 때 리플렉션 사용!
리플렉션 사용 : 공통 로직 합치기
@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);
}
- classHello.getMethod("call") : 해당 클래스의 call 메서드 메타정보를 획득한다.
- methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다
☆ 주의 : 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
JDK 동적 프록시 - 소개 + 예제
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.
JDK 동적 프록시가 제공하는 InvocationHandler 를 구현해서 작성하면 된다.
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[] args : 메서드를 호출할 때 전달한 인수
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("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;
}
}
dynamicA() 출력 결과
TimeInvocationHandler - TimeProxy 실행
AImpl - A 호출
TimeInvocationHandler - TimeProxy 종료 resultTime=0
JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1
※ 정리
- AImpl , BImpl 각각 프록시를 만들지 않았다.
- 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler 는 공통으로 사용했다.
JDK 동적 프록시 - 적용
LogTraceBasicHandler
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;
}
}
}
DynamicProxyBasicConfig
@Configuration
public class DynamicProxyBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));
OrderControllerV1 proxy = (OrderControllerV1)
Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(), new Class[]{OrderControllerV1.class},
new LogTraceBasicHandler(orderController, logTrace)
);
return proxy;
}
//OrderServiceV1
//OrderRepositoryV1
문제 : no-log() 메서드도 LogTraceBasicHandler 적용됨.
메서드 이름 필터 기능 추가
LogTraceFilterHandler
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;
}
}
}
- PatternMatchUtils.simpleMatch(..) 를 사용하면 단순한 매칭 로직을 쉽게 적용할 수 있다.
xxx : xxx가 정확히 매칭되면 참
xxx* : xxx로 시작하면 참
*xxx : xxx로 끝나면 참
*xxx* : xxx가 있으면 참 - String[] patterns : 적용할 패턴은 생성자를 통해서 외부에서 받는다.
DynamicProxyFilterConfig
@Configuration
public class DynamicProxyFilterConfig {
public static final String[] PATTERNS = {"request*", "order*", "save*"};
// OrderControllerV1
// OrderServiceV1
// OrderRepositoryV1
}
→ no-log 가 사용하는 noLog() 메서드에는 로그 남지 않는다.
JDK 동적 프록시 - 한계
JDK 동적 프록시는 인터페이스가 필수
해결 방안 : CGLIB
CGLIB - 소개 + 예제
CGLIB : Code Generator Library
→ 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
CGLIB가 제공하는 MethodInterceptor 를 구현해서 작성하면 된다.
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 메서드를 호출하면서 전달된 인수
- proxy : 메서드 호출에 사용
TimeMethodInterceptor
@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 proxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = proxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
→ proxy.invoke(target, args) : 실제 대상을 동적으로 호출한다.
실행 결과
CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
TimeMethodInterceptor - TimeProxy 실행
ConcreteService - ConcreteService 호출
TimeMethodInterceptor - TimeProxy 종료 resultTime=9
→ 대상클래스$$EnhancerByCGLIB$$임의코드
CGLIB가 생성한 프록시 클래스 이름
ex) ConcreteService$$EnhancerByCGLIB$$25d6b0e3
JDK 프록시 클래스가 생성한 이름
ex) proxyClass=class com.sun.proxy.$Proxy1
CGLIB 제약
- 부모 클래스의 생성자를 체크해야 한다. → CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생!
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
문제
인터페이스가 있는 경우에는 JDK 동적 프록시(InvocationHandler)를 적용,
인터페이스가 없는 경우에는 CGLIB(MethodInterceptor)를 적용하려면
중복해서 만들어서 관리해야할까...?
'Spring > Advanced' 카테고리의 다른 글
빈 후처리기 (0) | 2023.08.17 |
---|---|
스프링이 지원하는 프록시 (0) | 2023.08.17 |
프록시 패턴과 데코레이터 패턴 (0) | 2023.08.14 |
템플릿 메서드 패턴과 콜백 패턴 (0) | 2023.08.13 |
쓰레드 로컬 - ThreadLocal (0) | 2023.08.12 |