⬜섹션5 동적 프록시 기술
◼️리플렉션
- 리플랙션을 사용해 클래스나 메서드 메타정보를 사용해서 동적으로 호출하는 메서드를 변경할 수 있다.
아래 코드 를 보면, 중복되는 부분이 나타난다. 중복부분을 제거하려면 메서드로 뽑아서 공통화 해야 한다.
하지만 중간 호출 메서드가 다르다.
🔶중복이 많은 코드
@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";
}
}
}
🔶메서드 정보를 획득해서 메서드 호출
아래처럼 메서드 메타정보를 획득해, 인스턴스를 넘겨주는 방식이 있다. 이를 통해 동적 변경이 가능하다.
@Test
void reflection1() throws Exception {
//클래스 정보
//내부 클래스는 구분을 위해 $을 사용
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA"); // call메서드 메타정보 획득
Object result1 = methodCallA.invoke(target); // 인스턴스를 넘겨주기
log.info("result1={}",result1);
//callA 메서드 정보
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result2={}",result1);
}
위를 이용해 dynamicCall() 함수를 만들고, 메서드 정보를 동적으로 제공할 수 있다.
@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);
}
🤔하지만 리플렉션 기술은 런타임에 동작한다! 따라서 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
◼️JDK 동적 프록시
🟢동적 프록시 기술
- 개발자가 직접 프록시 클래스를 만들지 않아도, 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다.
- 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
- JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들기때문에, 인터페이스가 필수이다.
JDK 동적 프록시는 InvocationHandler 인터페이스를 구현해서 작성한다.파라미터는 순서대로 프록시,프록시 객체에서 호출할 메서드, 메서드 호출할때 전달할 인수이다.
🔶 InvocationHandler Interface
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
🔶 InvocationHandler Interface 구현
@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();
// 리플렉션으로 target 인스턴스의 메서드 실행 , args; 메서드호출시 넘겨줄 인수.
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
@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());
}
}
위 코드에서는 newProxyInstance로 동적 프록시를 생성한다. 이때 프록시를 적용할 클래스 로더 정보, 인터페이스, 마지막으로 프록시 핸들러 로직을 넣어준다.
동적 프록시 call()을 실행하면 핸들러의 invoke()를 호출하고, 내부 로직실행 후 method.invoke()로 실제 객체를 호출한다.
동적 프록시 전후 차이는 다음과 같다.
◼️JDK 동적 프록시 - 적용
🔶 LogTrace 적용할 수 있는 InvocationHandler 생성
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 {
//method 를 통해서 호출되는 메서드 정보와 클래스 정보를 동적으로 확인 가능
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 로 수동 빈 등록
동적 프록시를 만들더라도 LogTrace를 출력하는 로직은 모두 같기에, LogTraceBasicHandler를 사용하면 된다.
@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;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 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;
}
}
🔶 동적 프록시 설정 @Import 및 실행
@Import(DynamicProxyBasicConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace(){
return new ThreadLocalLogTrace();
}
}
직접 프록시를 사용했을때와 JDK 동적 프록시 사용을 했을때 차이를 비교해보자.
◼️JDK 동적 프록시 - no log 조건 적용
추가로, 요구사항에 따른 추가 기능을 개발해보자. /no-log 를 호출했을때 로그를 남기지 않도록 해보자.
여기서 중요한 것은, no-log 시에도 method.invoke()로 본 로직은 호출해야 한다는 것이다.
🔶 필터 조건 추가
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;
}
}
}
🔶 DynamicProxyFilterConfig
@Configuration
public class DynamicProxyFilterConfig {
private static final String[] PATTERNS = {"request*", "order*", "save*"};
@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 LogTraceFilterHandler(orderController, logTrace, PATTERNS)
);
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 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;
}
}
@Import(DynamicProxyFilterConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app.v3") //주의
public class ProxyApplication {
public static void main(String[] args) {
SpringApplication.run(ProxyApplication.class, args);
}
@Bean
public LogTrace logTrace(){
return new ThreadLocalLogTrace();
}
}
◼️CGLIB
JDK 동적 프록시는 인터페이스가 필수이다. 따라서 인터페이스 없이 클래스만 있는경우에는 CGLIB을 사용해야 한다.
🟢CGLIB: Code Generator Library
- CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
- CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
- CGLIB는 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
- CGLIB가 동적으로 생성하는 클래스이름은 [대상클래스$$EnhancerByCGLIB$$임의코드] 로 생성했다.
🟢CGLIB 제약
- CGLI클래스 기반 프록시는 상속을 사용하기 때문에 제약이 있다.
- 부모 클래스의 생성자를 체크해야 한다. → CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속 불가능 → CGLIB에서는 예외
- 메서드에 final 키워드가 붙으면 메서드 오버라이딩 불가 → CGLIB에서는 프록시 로직 동작 X
또한 CGLIB을 사용하면 인터페이스가 없는 V2 애플리케이션에 동적 프록시를 적용할 수 있다.
JDK 동적 프록시가 실행 로직을 위해 InvocationHandler 를 제공했듯이, CGLIB는 MethodInterceptor를 제공한다.
🔶 cglib 적용 예시
@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;
}
}
@Slf4j
public class CglibTest {
@Test
void cglib() {
ConcreteService target = new ConcreteService();
// CGLIB; Enhancer 를 사용해서 프록시를 생성
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();
}
}
'Spring' 카테고리의 다른 글
[Spring] 스프링 핵심 원리 - 고급편 섹션7 빈 후처리기 (0) | 2024.08.14 |
---|---|
[Spring] 스프링 핵심 원리 - 고급편 섹션6 스프링이 지원하는 프록시 (0) | 2024.08.04 |
[Spring] 스프링 핵심 원리 - 고급편 섹션4 프록시 패턴과 데코레이터 패턴 (0) | 2024.07.27 |
[Spring] 스프링 핵심 원리 - 고급편 섹션3 템플릿 메서드 패턴과 콜백 패턴 (2) | 2024.07.24 |
[Spring] 스프링 핵심 원리 - 고급편 섹션2 쓰레드 로컬 - ThreadLocal (0) | 2024.07.19 |