- 스프링 핵심원리 고급편 예제만들기
- 스프링 핵심원리 고급편(2) 쓰레드 로컬(threadlocal)
- 스프링 핵심원리 고급편(3) 템플릿 메서드 패턴과 콜백 패턴
- 스프링 핵심원리 고급편 프록시 패턴과 데코레이터 패턴
- 스프링 핵심원리 고급편(2) 동적 프록시 기술
- 스프링 핵심원리 고급편 스프링이 지원하는 프록시
- 스프링 핵심원리 고급편(2) 빈 후처리기
- 스프링 핵심원리 고급편(3) @aspect aop
- 스프링 핵심원리 고급편(3) 스프링 aop 구현
- 스프링 핵심원리 고급편 스프링 aop 포인트컷
- 스프링 핵심원리 고급편 스프링 aop 실전 예제
- 스프링 핵심원리 고급편(2) 스프링 aop 실무 주의사항
스프링 핵심원리 고급편(2) 동적 프록시 기술
0. 리플렉션
- 자바가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다.
- JDK 동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 한다. 리플렉션 기술을 사용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
ReflectionTest
-
package hello.proxy.jdkdynamic; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @Slf4j public class ReflectionTest { @Test void reflection0() { Hello target = new Hello(); //공통 로직 1 시작 log.info("start"); String result1 = target.callA(); log.info("result1={}", result1); //공통 로직 1 종료 // // 공통 로직 2 시작 log.info("start"); String result2 = target.callB(); log.info("result2={}", result2); //공통 로직 2 종료 } @Slf4j static class Hello{ public String callA(){ log.info("callA"); return "A"; } public String callB(){ log.info("callB"); return "B"; } } }
- reflection0() : 공통 로직1과 공통 로직2는 호출하는 메서드만 다르고 전체 코드 흐름이 완전히 같다.
- 여기서 공통 로직1과 공통 로직 2를 하나의 메서드로 뽑아서 합치는 건 쉬워 보이지만 메서드로 뽑아서 공통화하는 것이 생각보다 어렵다. 왜냐하면 중간에 호출하는 메서드가 다르기 때문이다.
- 이럴 때 사용하는 기술이 바로 리플렉션이다.
ReflectionTest - reflection1 추가
-
@Test void reflection1() throws Exception { //클래스 정보 Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello"); Hello target = new Hello(); //callA 메서드 정보 Method methodCallA = classHello.getMethod("callA"); Object result1 = methodCallA.invoke(target); log.info("result={}", result1); //callB 메서드 정보 Method methodCallB = classHello.getMethod("callB"); Object result2 = methodCallB.invoke(target); log.info("result={}", result1); } @Slf4j static class Hello{ public String callA(){ log.info("callA"); return "A"; } public String callB(){ log.info("callB"); return "B"; } }
- Class.forName(“hello.proxy.jdkdynamic.ReflectionTest$Hello”) : 클래스 메타정보를 획득한다. 참고로 내부 클래스는 구분을 위해 $ 를 사용한다.
- classHello.getMethod(“call”) : 해당 클래스의 call 메서드 메타정보를 획득한다.
- methodCallA.invoke(target) : 획득한 메서드 메타정보로 실제 인스턴스의 메서드를 호출한다. 여기서 methodCallA 는 Hello 클래스의 callA() 이라는 메서드 메타정보이다.
- methodCallA.invoke(인스턴스) 를 호출하면서 인스턴스를 넘겨주면 해당 인스턴스의 callA() 메서드를 찾아서 실행한다. 여기서는 target 의 callA() 메서드를 호출한다
여기서 중요한 핵심은 클래스나 메서드 정보를 동적으로 변경할 수 있다는 점이다
ReflectionTest - reflection2 추가
-
@Test void reflection2() throws Exception { //클래스 정보 Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello"); Hello target = new Hello(); //callA 메서드 정보 Method methodCallA = classHello.getMethod("callA"); dynamicCall(methodCallA, target); //callB 메서드 정보 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("result1={}", result); } @Slf4j static class Hello{ public String callA(){ log.info("callA"); return "A"; } public String callB(){ log.info("callB"); return "B"; } }
-
dynamicCall(Method method, Object target)
- 공통 로직1, 공통 로직2를 한번에 처리할 수 있는 통합된 공통 처리 로직이다.
- Method method : 클래스의 메서드 메타 정보
- Object target : 실제 실행할 인스턴스 정보가 넘어온다. 타입이 Object 라는 것은 어떠한 인스턴스도 받을 수 있다는 뜻이다.
주의
- 리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다.
- 따라서 리플렉션은 일반적으로 사용하면 안된다. 리플렉션은 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.
1. JDK 동적 프록시 - 소개
기본 예제 코드 작성 (src/test)
AInterface / AImpl
-
package hello.proxy.jdkdynamic.code; a public interface AInterface { String call(); }
-
package hello.proxy.jdkdynamic.code; import lombok.extern.slf4j.Slf4j; @Slf4j public class AImpl implements AInterface{ @Override public String call() { log.info("A 호출"); return "a"; } }
BInterface / BImpl
-
package hello.proxy.jdkdynamic.code; public interface BInterface { String call(); }
-
package hello.proxy.jdkdynamic.code; import lombok.extern.slf4j.Slf4j; @Slf4j public class BImpl implements BInterface{ @Override public String call() { log.info("B 호출"); return "b"; } }
2. JDK 동적 프록시 - 예제 코드 (src/test)
- JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.
JDK 동적 프록시가 제공하는 InvocationHandler
-
package java.lang.reflect; public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; }
제공되는 파라미터는 다음과 같다.
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[] args : 메서드를 호출할 때 전달한 인수
TimeInvocationHandler
-
package hello.proxy.jdkdynamic.code; import lombok.extern.slf4j.Slf4j; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; @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); //call() long endTime = System.currentTimeMillis(); long resultTime = endTime - startTime; log.info("TimeProxy 종료 resultTime={}", resultTime); return result; } }
-
TimeInvocationHandler 은 InvocationHandler 인터페이스를 구현한다. 이렇게해서 JDK 동적 프록시에 적용할 공통 로직을 개발할 수 있다.
-
Object target : 동적 프록시가 호출할 대상
- method.invoke(target, args) : 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다. args 는 메서드 호출시 넘겨줄 인수이다.
JdkDynamicProxyTest
-
package hello.proxy.jdkdynamic; import hello.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); //동적 프록시는 java.lang.reflect.Proxy 를 통해서 생성할 수 있다. //클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다. 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()); } }
dynamicA() 출력 결과
-
TimeInvocationHandler - TimeProxy 실행 AImpl - A 호출 TimeInvocationHandler - TimeProxy 종료 resultTime=0 JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1
생성된 JDK 동적 프록시
- proxyClass=class com.sun.proxy.$Proxy1 이 부분이 동적으로 생성된 프록시 클래스 정보이다. 이것은 우리가 만든 클래스가 아니라 JDK 동적 프록시가 이름 그대로 동적으로 만들어준 프록시이다. 이 프록시는 TimeInvocationHandler 로직을 실행한다.
실행 순서
- 클라이언트는 JDK 동적 프록시(proxy)의 call() 을 실행한다. (AImpl 에 있는 메서드 이름으로 실행)
- JDK 동적 프록시(proxy)는 InvocationHandler.invoke() 를 호출한다. TimeInvocationHandler 가 구현체로 있으로 TimeInvocationHandler.invoke() 가 호출된다.
- TimeInvocationHandler 가 내부 로직을 수행하고, method.invoke(target, args) 를 호출해서 target 인 실제 객체( AImpl )를 호출한다.
- AImpl 인스턴스의 call() 이 실행된다.
- AImpl 인스턴스의 call() 의 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
정리
- 예제를 보면 AImpl , BImpl 각각 프록시를 만들지 않았다. 프록시는 JDK 동적 프록시를 사용해서 동적으로 만들고 TimeInvocationHandler 는 공통으로 사용했다.
- JDK 동적 프록시 기술 덕분에 적용 대상 만큼 프록시 객체를 만들지 않아도 된다.
JDK 동적 프록시 도입 후
3. JDK 동적 프록시 - 적용1
- JDK 동적 프록시는 인터페이스가 필수이기 때문에 V1 애플리케이션에만 적용할 수 있다.
LogTraceBasicHandler
-
먼저 LogTrace 를 적용할 수 있는 InvocationHandler 를 만들자.
-
package hello.proxy.config.v2_dynamicproxy; import hello.proxy.trace.TraceStatus; import hello.proxy.trace.logtrace.LogTrace; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; 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{ //LogTrace 에 사용할 메시지이다. //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
-
동적 프록시를 사용하도록 수동 빈 등록을 설정하자.
-
package hello.proxy.config.v2_dynamicproxy; import hello.proxy.app.v1.*; import hello.proxy.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 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; } }
newProxyInstance 매개변수 (from gpt)
- ClassLoader loader: 다이나믹 프록시 클래스를 로딩하는 데 사용되는 클래스 로더입니다. 다이나믹 프록시 클래스는 런타임 시간에 생성되므로, 기존에 컴파일된 클래스 파일에서는 찾을 수 없습니다. 따라서, 프록시 클래스를 로딩하기 위해서는 새로운 클래스 로더가 필요합니다.
- Class<?>[] interfaces: 프록시 객체가 구현해야 하는 인터페이스들의 목록입니다. 다이나믹 프록시 객체는 특정 인터페이스를 구현하는 객체이므로, 구현해야 하는 인터페이스를 명시해야 합니다. 이 인터페이스 목록은 배열 형태로 전달됩니다.
- InvocationHandler h: 다이나믹 프록시 객체에서 메서드 호출 시 실행되는 로직을 정의하는 InvocationHandler 객체입니다. InvocationHandler 인터페이스는 invoke() 메서드를 정의하고 있으며, 다이나믹 프록시 객체에서 메서드가 호출될 때마다 이 메서드가 실행됩니다. invoke() 메서드는 실제 메서드가 호출되기 전에 호출되며, 매개변수로는 프록시 객체, 호출된 메서드, 호출된 메서드의 매개변수 등이 전달됩니다.
오류났던 것
- newProxyInstance 2번째 매개변수로
orderControllerV1.getClass()
이라고 넣었다. orderControllerV1 = new orderControllerV1Impl(orderServiceV1(logTrace));
로 구현되어 있으므로 값은 orderControllerV1Impl 이다.- 인터페이스 목록이 들어가야 하므로
OrderControllerV1.class
가 들어가야 한다.
.getClass() vs .class (from gpt)
- .getClass()와 .class 모두 클래스 정보를 가져오는 용도로 사용되는 메서드이지만, 사용 방법과 동작 방식이 다릅니다.
- .getClass()는 객체의 클래스 정보를 가져오는 메서드입니다. 즉, 해당 객체가 어떤 클래스의 인스턴스인지를 나타내는 클래스 정보를 가져옵니다. 이 메서드는 Object 클래스에 정의되어 있으므로 모든 객체에서 사용할 수 있습니다.
- 예를 들어, 다음과 같은 코드를 실행하면, 문자열 “Hello”가 String 클래스의 인스턴스인지 확인할 수 있습니다.
javaCopy codeString str = "Hello";
Class clazz = str.getClass();
System.out.println(clazz.getName()); // java.lang.String
- .class는 클래스의 정보를 나타내는 리터럴(literal)입니다. 이 리터럴을 사용하면, 해당 클래스의 클래스 정보를 가져올 수 있습니다.
- 예를 들어, 다음과 같은 코드를 실행하면, String 클래스의 클래스 정보를 가져올 수 있습니다.
javaCopy codeClass clazz = String.class;
System.out.println(clazz.getName()); // java.lang.String
즉, .getClass()는 객체를 생성한 클래스의 정보를 가져오고, .class는 클래스 자체의 정보를 가져옵니다.
실행
- ProxyApplication -> @Import(DynamicProxyBasicConfig.class)
실행 : http://localhost:8080/v1/request?itemId=hello
- 특 x
그림으로 정리
- 직접 프록시 사용
- JDK 동적 프록시 사용
4. JDK 동적 프록시 - 적용2
- 메서드 이름 필터 기능 추가
http://localhost:8080/v1/no-log
요구사항에 의해 이것을 호출 했을 때는 로그가 남으면 안된다.- 메서드 이름을 기준으로 특정 조건을 만족할 때만 로그를 남기는 기능을 개발해보자.
LogTraceFilterHandler
-
package hello.proxy.config.v2_dynamicproxy; import hello.proxy.trace.TraceStatus; import hello.proxy.trace.logtrace.LogTrace; import org.springframework.util.PatternMatchUtils; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; 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 { TraceStatus status = null; //메서드 이름 필터 String methodName = method.getName(); //save, request, reque*, *est if (!PatternMatchUtils.simpleMatch(patterns, methodName)) { return method.invoke(target, args); } try{ String message = method.getDeclaringClass().getSimpleName() + "." + method + "()"; 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(..) )
-
private final String[] patterns; //메서드 이름 필터 String methodName = method.getName(); //save, request, reque*, *est if (!PatternMatchUtils.simpleMatch(patterns, methodName)) { return method.invoke(target, args); }
- xxx : xxx가 정확히 매칭되면 참
- xxx* : xxx로 시작하면 참
- *xxx : xxx로 끝나면 참
- xxx : xxx가 있으면 참
- String[] patterns : 적용할 패턴은 생성자를 통해서 외부에서 받는다.
DynamicProxyFilterConfig
-
package hello.proxy.config.v2_dynamicproxy; import hello.proxy.trace.TraceStatus; import hello.proxy.trace.logtrace.LogTrace; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; 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; } } }
-
public static final String[] PATTERNS = {“request”, “order”, “save*”};
- 적용할 패턴이다. request , order , save 로 시작하는 메서드에 로그가 남는다.
- LogTraceFilterHandler : 앞서 만든 필터 기능이 있는 핸들러를 사용한다. 그리고 핸들러에 적용 패턴도 넣어준다.
실행
- @Import(DynamicProxyFilterConfig.class) 으로 방금 만든 설정을 추가한다.
- 실행
http://localhost:8080/v1/request?itemId=hello
http://localhost:8080/v1/no-log
- 실행해보면 no-log 가 사용하는 noLog() 메서드에는 로그가 남지 않는 것을 확인할 수 있다.
JDK 동적 프록시 - 한계
JDK 동적 프록시는 인터페이스가 필수이다. 그렇다면 V2 애플리케이션 처럼 인터페이스 없이 클래스만 있는 경우에는 어떻게 동적 프록시를 적용할 수 있을까? 이것은 일반적인 방법으로는 어렵고 CGLIB 라는 바이트코드를 조작하는 특별한 라이브러리를 사용해야 한다.
5. CGLIB - 소개
CGLIB: Code Generator Library
- CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
- CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
참고로 우리가 CGLIB를 직접 사용하는 경우는 거의 없다. 이후에 설명할 스프링의 ProxyFactory 라는 것이 이 기술을 편리하게 사용하게 도와주기 때문에, 너무 깊이있게 파기 보다는 CGLIB가 무엇인지 대략 개념만 잡으면 된다.
공통 예제 코드 (src/test)
- 인터페이스와 구현이 있는 서비스 클래스 - ServiceInterface , ServiceImpl
- 구체 클래스만 있는 서비스 클래스 - ConcreteService
ServiceInterface
-
package hello.proxy.common.service; public interface ServiceInterface { void save(); void find(); }
ServiceImpl
-
package hello.proxy.common.service; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j public class ServiceImpl implements ServiceInterface { @Override public void save() { log.info("save 호출"); } @Override public void find() { log.info("find 호출"); } }
ConcreteService
-
package hello.proxy.common.service; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @Slf4j public class ConcreteService { public void call(){ log.info("ConcreteService 호출"); } }
6. CGLIB - 예제 코드 (src/test)
- JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler 를 제공했듯이, CGLIB는 MethodInterceptor 를 제공한다.
MethodInterceptor -CGLIB 제공
-
package org.springframework.cglib.proxy; public interface MethodInterceptor extends Callback { Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable; }
- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 메서드를 호출하면서 전달된 인수
- proxy : 메서드 호출에 사용
TimeMethodInterceptor
-
package hello.proxy.cglib.code; import lombok.extern.slf4j.Slf4j; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; @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(); //methodProxy 로 실행 Object result = methodProxy.invoke(target, args); //call() long endTime = System.currentTimeMillis(); long resultTime = endTime - startTime; log.info("TimeProxy 종료 resultTime={}", resultTime); return result; } }
- TimeMethodInterceptor 는 MethodInterceptor 인터페이스를 구현해서 CGLIB 프록시의 실행 로직을 정의한다.
- Object target : 프록시가 호출할 실제 대상
- proxy.invoke(target, args) : 실제 대상을 동적으로 호출한다.
- 참고로 method 를 사용해도 되지만, CGLIB는 성능상 MethodProxy proxy 를 사용하는 것을 권장한다.
CglibTest
-
package hello.proxy.cglib; import hello.proxy.cglib.code.TimeMethodInterceptor; import hello.proxy.common.service.ConcreteService; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.springframework.cglib.proxy.Enhancer; @Slf4j public class CglibTest { @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(); } }
- Enhancer : CGLIB는 Enhancer 를 사용해서 프록시를 생성한다.
- enhancer.setSuperclass(ConcreteService.class) : CGLIB는 구체 클래스를 상속 받아서 프록시를 생성할 수 있다. 어떤 구체 클래스를 상속 받을지 지정한다.
- enhancer.setCallback(new TimeMethodInterceptor(target)) 프록시에 적용할 실행 로직을 할당한다. enhancer.create() : 프록시를 생성한다. 앞서 설정한 enhancer.setSuperclass(ConcreteService.class) 에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.
JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB는 구체 클래스를 상속 (extends)해서 프록시를 만든다.
실행 결과
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
CGLIB가 생성한 프록시 클래스 이름
- CGLIB를 통해서 생성된 클래스의 이름을 확인해보면,
- ConcreteService\(EnhancerByCGLIB\)25d6b0e3
- CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
- 대상클래스\(EnhancerByCGLIB\)임의코드
그림으로 정리
CGLIB 제약
- 클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- (JDK proxy 제약 -> CGLIB proxy 제)
- 부모 클래스의 생성자를 체크해야 한다. -> CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다. -> CGLIB에서는 예외가 발생한다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. -> CGLIB에서는 프록시 로직이 동작하지 않는다.
댓글남기기