스프링 AOP : 동적 프록시 적용(JDK 동적 프록시, CGLIB)
- Spring/Spring
- 2022. 1. 28.
이 글은 인프런의 김영한님 강의를 보고 복습하며 정리한 글입니다.
동적 프록시의 필요
앞선 글(https://ojt90902.tistory.com/699)에서 프록시 개념을 도입했다. 다형성을 이용해 프록시 개념을 많이 도입했고, 그 덕분에 원본 코드의 수정이 없이 로그 추적기를 필요한 클래스에 적용할 수가 있었다. 그렇지만 한계점은 명확했다.
한계점은 프록시를 도입하고자 하는 클래스만큼 프록시 클래스를 하나하나 만들어줘야한다는 것이다. 적용 클래스가 몇 개 정도면 만들어서 등록하면 되지만, 클래스가 수백개가 되면 사실 상 불가능한 일이다. 그렇다면 어떻게 해야할까?
앞선 글에서 작성되었던 프록시 객체의 코드다. 코드를 자세히 뜯어보면 다음을 알 수 있다.
- 부가 기능을 적용하는 코드가 거의 똑같다.
- 실제 타겟 객체를 불러오는 시점도 거의 동일하다.
코드는 거의 똑같은데 메서드나 다른 방법으로 뽑아내기가 쉽지 않다는 것을 알 수 있다. 왜냐하면 target이 호출하는 함수명이 다르기 때문이다. 왼쪽에서는 target.save()를 했고, 오른쪽에서는 target.orderItem()을 했다. 메서드 이름이 다르기 때문에 뽑아낼 수가 없다. 그렇다면 이대로 무너질 것인가?
자바의 리플렉션 활용
앞에서 볼 수 있듯이 단순히 메서드 이름만 다르다는 것을 볼 수 있다. 그렇다면 실행되는 시점에 동적으로 프록시를 생성하고 동적으로 메서드를 넘겨주면 된다는 것을 알 수 있다. 동적으로 프록시를 생성하려면 자바의 리플렉션 기술을 사용해야한다.
자바의 리플렉션 기술을 사용하면 클래스나 메서드의 메타 정보를 동적으로 획득하고, 이를 이용해서 동적 프록시를 생성해서 코드도 동적으로 호출할 수 있다.
@Test
void reflection1() throws Exception {
Hello hello = new Hello();
// 클래스 메타 정보를 받아온다.
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
// 클래스 메타 정보에서 메서드 명으로 검색해서 메서드를 뽑아온다.
Method methodCallA = classHello.getMethod("callA");
// 메서드를 실행하며, 인스턴스를 넘겨준다.
Object result1 = methodCallA.invoke(hello);
log.info("result1 = {}",result1);
}
위는 간단하게 자바의 리플렉션을 활용해 메서드를 동적으로 불러온 테스트 코드다. 여기서 알 수 있는 내용은 다음과 같다.
- 클래스 메타 정보를 받아오면, 그 메타 정보에서 이름으로 메서드를 뽑아올 수 있다.
- 메서드는 문자로 검색하는데, 클래스명은 인자로 넘겨 동적으로 검색할 수 있다.
- 반환받은 메서드에 객체를 넘겨서 실행해주면 원하는 결과를 얻을 수 있다.
자바 리플렉션은 안돼! JDK 동적 프록시, CGLIB을 이용해라
자바 리플렉션은 치명적인 단점이 있다. 바로 문자열로 전달되기 때문에 내가 잘못된 값을 전달하더라도 컴파일 시점에 오류를 검출할 수 없다는 것이다. 이런 이유 때문에 리플렉션은 유용한 기술이지만 사용하기가 굉장히 까다롭다. 리플렉션을 사용한 동적 메서드 호출은 사용하지 않도록 한다.
또 다른 방법은 JDK 동적 프록시, CGLIB를 이용하는 방법이다. JDK 동적 프록시와 CGLIB는 이런저런 차이가 있는데 가장 크게는 이런 차이가 있다고 보면 될 것 같다.
- JDK 동적 프록시
- InvocationHandler 인터페이스를 구현해서 사용.
- 반드시 인터페이스가 존재해야한다.
- CGLIB 동적 프록시
- MethodInterceptor 인터페이스를 구현해서 사용.
- 인터페이스가 없어도 구현할 수 있다.
JDK 동적 프록시 사용 예제 맛보기 → 반드시 인터페이스가 있어야함.
JDK 동적 프록시는 앞서 이야기한 것처럼 자바의 리플렉션 기술을 활용해서 동적으로 프록시를 생성한다. 동적으로 프록시를 생성하려면 다음과 같은 방법으로 해야한다.
- InvocationHandler 인터페이스를 구현한 구현체를 만든다.
- 프록시가 참조할 실제 객체를 만든다.
- Proxy.newProxyInstance 정보를 통해서 프록시 객체를 가져온다. 이 때, 클래스 로더와 클래스 정보의 메타 정보를 가져온다.
리플렉션이 클래스의 메타정보를 바탕으로 메서드를 동적으로 불러온다는 것을 감안한다면, 그것과 크게 다르지 않다.
InvocationHandler를 구현한 클래스 → 동적 플래시의 몸통 역할
@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를 구현하면 됨.
- InvocationHandler는 프록시 객체의 뼈대가 된다. 따라서 내부적으로 참조할 타겟을 가져야 함.
- InvocationHandler의 Invoke에는 Proxy 정보, 동적 메서드 정보, 인수가 함께 넘어옴.
- method.Invoke()를 통해서 실제 객체를 불러올 수 있다. 이 때, invoke에 Target과 인수를 넘겨주면 됨.
테스트 코드에서 프록시 객체 동적으로 생성하기
@Test
void dynamicJdkA() {
// 타겟 객체 생성
AImpl a = new AImpl();
// 프록시 객체 몸통 만들기
TimeInvocationHandler handler = new TimeInvocationHandler(a);
// 메타 정보와 프록시 몸통 정보 넘겨주고 프록시 만들기
AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(),
new Class[]{AInterface.class},
handler);
// 동적 프록시를 통해 메서드 정보가 넘어감.
proxy.call();
}
- 넘겨줄 타겟을 만든다.
- 프록시 몸통을 만든다. 이 때 이미 InvocationHandler를 구현해둔 객체를 생성한다. 이 때 타겟값을 넘겨준다.
- 프록시 몸통은 이제 타겟이 뭔지 안다.
- 3. Proxy.newProxyInstance에 클래스 로더 정보, 클래스 정보, 프록시 몸통 정보를 넘겨주고 프록시를 생성한다.
- 4. Proxy.call()을 하면 프록시를 호출한다.
- 프록시를 호출하게 되면 자동으로 InvocationHandler의 invoke 메서드가 실행된다.
- 이 때, 메서드명 + 클래스 메타 정보를 통해 메서드가 invoke에 넘어간다.
- 2번에 의거해서 프록시는 이미 타겟을 알고 있다.
- 따라서 프록시 객체는 동적으로 원하는 타겟, 원하는 메서드를 뽑아와서 실행한다.
실행 순서를 좀 더 체계적으로 정리하면 다음과 같다.
- 클라이언트는 JDK 동적 프록시의 call() 을 실행한다.
- JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다. TimeInvocationHandler 가 구현체로 있으로 TimeInvocationHandler.invoke() 가 호출된다.
- TimeInvocationHandler 가 내부 로직을 수행하고, method.invoke(target, args) 를 호출해서 target 인 실제 객체( AImpl )를 호출한다.
- AImpl 인스턴스의 call() 이 실행된다.
- AImpl 인스턴스의 call() 의 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
실행순서를 그림으로 옮기면 다음과 같다.
JDK 동적 프록시로 만들어지는 프록시 객체 확인하기
- 위 이미지는 타겟 클래스와 JDK 동적 프록시로 생성한 프록시를 .getClass()로 확인한 것이다.
- JDK 동적 프록시는 "com.sun.proxy" 라는 이름으로 만들어지는 것을 알 수 있다.
정리
JDK 동적 프록시를 이용하면 '클래스 정보, 클래스 로더 정보, 타겟 정보'를 넘겨줘서 동적으로 프록시를 생성할 수 있다. 이 때 InvocationHandler를 구현한 구현체를 만들어야 하는데, 이 구현체는 각 프록시 객체가 사용할 몸뚱아리라고 보면 된다.
JDK 동적 프록시로 만들어진 프록시 객체를 통해 메서드를 실행하면, 메서드는 자동으로 InvocationHandler.invoke() 메서드를 실행한다. 따라서 필요한 프록시 기능은 InvocationHandler.Invoke()안에 구현해두면 된다. 결과적으로 핵심 기능과 부가 기능을 나누게 되며 단일 책임 원칙(SRP)를 지킬 수 있게 되었고, 프록시 클래스를 수 없이 만들어야 하는 문제도 해결할 수 있다.
JDK 동적 프록시 도입 전에는 이렇게 각 인터페이스마다 그에 맞는 프록시를 하나씩 생성해서 의존관계를 설정해야했다.
JDK 동적 프록시를 도입한 다음부터, 개발자는 공통으로 쓰일 프록시 몸뚱아리인 InvocationHandler만 구현하고, 의존관계를 주입해주기만 하면 된다. 즉, 프록시 인터페이스 하나만 만들어서 같이 나눠쓰면 된다.
동적 프록시 도입 전, 런타임에서 클라이언트는 aProxy에 의존, 그리고 aProxy는 실제 객체에 의존했다.
동적 프록시를 도입하게 되면, 클라이언트가 동적 프록시를 호출하는 순간 InvocationHandler.invoke()가 실행되는데 이 때 메서드 메타 정보가 동적으로 전달된다. 그리고 이 메타정보는 invoke 메서드 내에서 method.invoke()를 통해서 동적으로 실행할 수 있게 된다.
JDK 동적 프록시, 로그 추적기 도입하기
앞서 테스트 코드에서 JDK 동적 프록시를 구현하는 과정에서 정리한 내용은 다음과 같다.
- JDK 동적 프록시를 생성하려면 클래스 메타 정보, 클래스 정보, 프록시 몸통 정보가 주어져야한다.
- JDK 동적 프록시로 생성된 프록시를 실행하면, 실행하는 시점에 InvocationHandler.invoke()가 실행되면서, 동적으로 메서드명이 넘어간다.
- 프록시의 몸통은 InvocationHandler의 구현체다. 이 때 내부적으로 Target을 갖도록 설계해서 어떤 타겟을 실행해야하는지 알고 있다.
- JDK 동적 프록시를 생성하려면, 반드시 인터페이스가 존재해야한다.
이렇게 하면 JDK 동적 프록시를 통해 넘어온 메서드를 동적으로 실행할 수 있기 때문에 이제 반복적으로 프록시 클래스를 만들 필요가 없어졌다.
로그 추적기 적용을 위한 InvocationHandler 구현
InvocationHandler는 프록시 객체가 사용할 몸통이다. 따라서, 기존의 프록시 형태를 가져야한다. 내부적으로 Target 객체를 가지도록 한다. 그리고 메서드를 오버라이딩해준다. 타겟의 실행은 method.invoke(target, args)로 실행하도록 한다.
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;
}
}
}
- 내부적으로 타겟 참조값을 가졌다.
- 생성자를 통해 의존관계 주입을 해준다.
- method.invoke(target, args)를 통해 실제 객체를 실행해준다.
JDK 동적 프록시의 의존관계 설정
@Configuration
public class LogTraceBasicConfig {
@Bean
public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
OrderControllerV1 target = new OrderControllerV1Impl(orderServiceV1(logTrace));
// 프록시 몸통 완성
LogTraceBasicHandler handler = new LogTraceBasicHandler(target, logTrace);
// 클래스 메타 정보 + 프록시 몸통 정보 넘겨줌. 프록시 생성
OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
new Class[]{OrderControllerV1.class}, handler);
return proxy;
}
@Bean
public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
OrderServiceV1 target = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
// 프록시 몸통 완성
LogTraceBasicHandler handler = new LogTraceBasicHandler(target, logTrace);
// 클래스 메타 정보 + 프록시 몸통 정보 넘겨줌. 프록시 생성
OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
new Class[]{OrderServiceV1.class}, handler);
return proxy;
}
@Bean
public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
OrderRepositoryV1 target = new OrderRepositoryV1Impl();
// 프록시 몸통 완성
LogTraceBasicHandler handler = new LogTraceBasicHandler(target, logTrace);
// 클래스 메타 정보 + 프록시 몸통 정보 넘겨줌. 프록시 생성
OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
new Class[]{OrderRepositoryV1.class}, handler);
return proxy;
}
}
- 먼저 실제 객체를 하나 생성한다.
- 프록시 몸통(InvocationHandler)에 실제 객체를 넘겨줘 참조 가능하도록 한다.
- Proxy.newProxyInstance에 클래스 로더 정보, 클래스 정보, 프록시 몸통 정보를 던져줘 프록시를 생성한다.
- 만들어진 프록시 객체를 반환한다.
→ 이렇게 되면 orderRepositoryV1이라는 빈 이름을 가진 스프링 빈에는 orderRepositoryV1Impl 객체를 참조로 가진 프록시 객체가 스프링 빈으로 등록된다.
정리
직접 클래스 별로 프록시 객체를 만들어서 등록하면, 다음과 같은 클래스 의존관계를 가진다. 인터페이스를 프록시 객체가 구현하고, 바꿔치기를 이용한 코드 조작이다.
JDK 동적 프록시를 사용하면, 각 클래스에 대한 구현체를 만들 필요가 없다. 왜냐하면 InvocationHandler라는 프록시 몸뚱아리를 구현한 것이 있고, 이것을 공용으로 사용할 수 있기 때문이다.
직접 프록시를 사용하면 클라이언트는 프록시 객체를 호출한다. 프록시 객체는 내부적으로 실제 객체를 참조하고 있고, 이 실제 객체를 실행해준다. 또 이 실제 객체는 다른 프록시 객체를 참조하고 하면서 내부적으로 프록시 체이닝이 발생한다.
JDK 동적 프록시를 사용하면, 클래스가 동적 프록시를 호출하는 순간 InvocationHandler.invoke()가 실행된다. 이 때, 이미 타겟 정보를 알고 있는 상황이고 메서드가 동적으로 넘어온다. 그리고 그 동적으로 넘어온 메서드가 method.invoke()가 되면서 실제 타겟이 실행되는 방향이 된다.
로그 추적기 적용을 위한 InvocationHandler 구현 → 유사 포인트컷
내부적으로 String 문자열을 가져 포인트 컷을 가져갈 수도 있다. 특정 String 문자열이 내가 실행할 메서드의 이름이라고 지정한다. 그래서 이 이름과 패턴이 매칭되지 않으면, 부가 기능을 실행하지 않도록 할 수 있다.
public class LogTraceFilterHandler implements InvocationHandler {
/.../
private final String[] patterns;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
// 포인트컷 구현 지점
if (!PatternMatchUtils.simpleMatch(patterns, name)) {
return method.invoke(target);
}
// 성공 로직
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;
}
}
}
위 InvocationHandler가 포인트컷을 원시적으로 구현한 형태다. 넘어온 문자열 패턴과 현재 동적으로 넘어온 메서드의 이름이 매치되지 않으면, 실행하지 않는다.
그런데 한 가지 주의해야할 부분이 있다. 바로 매칭이 되지 않았을 때, 전체 프로그램의 흐름은 정상적으로 돌아가야한다는 점이다. 부가 기능은 실행하지 않고, 타겟은 실행해야한다. 따라서 매칭이 되지 않았을 때라도 method.invoke()는 반드시 실행해줘야한다.
JDK 동적 프록시의 한계
JDK 동적 프록시의 한계는 '인터페이스가 반드시 필요하다'라는 점이다. 실무에서는 항상 인터페이스만 있지는 않다. 구현체만 있는 곳에서도 동적으로 프록시를 생성해야한다. 인터페이스 없이 구현체만 있는 클래스의 프록시를 동적으로 생성하기 위해서 CGLIB 라는 라이브러리를 사용한다.
CGLIB : Code Generator Library
CBLIG는 바이트 코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리다. 따라서 인터페이스가 없이 구체 클래스만으로 필요한 동적 프록시를 만들어낼 수 있다. 따라서 구체 클래스만 있을 경우 CGLIB 기술을 통해서 동적 프록시를 생성하도록 한다.
그렇지만 깊게 공부할 필요는 없다. 왜냐하면 이미 스프링은 동적 프록시 코드 추상화를 ProxyFactory라는 것을 제공해준다. 이 ProxyFactory에 CGLIB 기술을 도입해서 구체 클래스를 동적 프록시를 생성해준다. 따라서 추상화 되었기 때문에 실제로 CGLIB가 어떻게 구체 클래스를 동적 프록시로 만드는지를 자세하게는 알지 않아도 괜찮다.
CGLIB 라이브러리 이용한 동적 프록시 생성 테스트 코드
CGLIB도 JDK 동적 프록시 생성과 큰 맥락에서 다르지 않다. 프록시 몸뚱아리를 만들어주고, 그 몸뚱아리에 타겟을 넘겨준다. 그리고 일련의 과정을 통해 프록시 객체를 만들어내야한다. 프록시 몸뚱아리는 다음과 같다.
- JDK 동적 프록시 몸뚱아리 : InvocationHandler의 구현
- CGLIB 동적 프록시 몸뚱아리 : MethodInterceptor의 구현
따라서 MethodInterceptor를 구현해주고, 그걸 프록시를 만드는 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 o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
log.info("프록시 객체 실행");
long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("프록시 객체 종료, resultTime = {}",resultTime);
return result;
}
}
- JDK 동적 프록시의 InvocationHandler와 거의 구조가 동일하다.
- method.invoke()도 가능한데,methodProxy.invoke()로 하는 것이 성능이 좀 더 나온다고 한다.
CGLIB로 프록시 뽑아내기
@Test
void cglibTest() {
// 타겟 생성
ConcreteService target = new ConcreteService();
// 몸뚱아리 생성
TimeMethodInterceptor timeMethodInterceptor = new TimeMethodInterceptor(target);
// CGLIB 프록시 생성자 만들기
Enhancer enhancer = new Enhancer();
// 어떤 구체 클래스를 만들지 알려줌.
enhancer.setSuperclass(ConcreteService.class);
// 몸뚱아리 정보를 CGLIB 생성자에게 알려줌
enhancer.setCallback(timeMethodInterceptor);
// CGLIB 생성자로부터 프록시 가져오기
ConcreteService proxy = (ConcreteService)enhancer.create();
proxy.call();
log.info("target = {}", target.getClass());
log.info("proxy = {}", proxy.getClass());
}
- 타겟을 먼저 생성한다.
- MethodInterceptor 구체 클래스를 생성하면서 타겟을 넘겨준다. 이 때, 생성되는 것은 프록시 몸뚱아리 정보다.
- CGLIB 프록시 생성자 역할을 할 Enhancer()를 만든다.
- Enhancer에 setCallBack을 통해 프록시 몸뚱아리 정보를 넘겨준다.
- Enhancer에 어떤 클래스를 대상으로 만들지 정보를 알려준다.
- Enhacer를 통해 프록시를 생성한다.
위의 과정을 통해 CGLIB를 사용해서 동적으로 프록시를 뽑아낼 수 있게 되었다.
CGLIB로 생성한 클래스 살펴보기
타겟 클래스와 CGLIB로 생성한 프록시의 클래스를 찍어보았다. CGLIB로 생성한 프록시 객체는 EnhancerbyCGLIB라는 것이 붙는 것을 알 수 있다.
CGLIB 정리해보기
CGLIB의 클래스 의존 관계는 다음과 같다. Enhancer의 SuperClass로 ConcreteService에 의존하는 것을 알려주어 프록시를 만들었다. 그리고 이 CGLIB에 프록시 몸뚱아리(MethodInterceptor)를 던져주었다.
런타임 객체 의존 관계도 JDK와 크게 다르지 않다. 클라이언트가 CGLIB 프록시를 호출하면, CGLIB 호출은 바로 MethodInterceptor.interceptor()를 바로 실행해준다. 그리고 interceptor()안에서 Method.invoke() 혹은 MethodProxy.invoke()를 통해서 실제 객체를 실행해준다.
JDK 동적 프록시, CGLIB 동적 프록시 정리
- JDK 동적 프록시
- 자바의 리플렉션 기술로 동적 프록시를 만든다.
- 클래스 로더 정보, 클래스 정보, 프록시 몸통 정보, 타겟 정보를 넘겨서 동적으로 프록시를 생성해 메서드를 실행한다.
- 프록시 몸통 정보는 InvocationHandler를 구현한다. 이것은 공통으로 사용될 프록시 몸통이다.
- 생성은 Proxy.newProxyInstance()에 정보를 넘겨서 만든다.
- 반드시 프록시 객체를 생성할 타겟의 인터페이스가 있어야 한다.
- JDK 동적 프록시 객체를 호출하면 invocationHandler.invoke()가 바로 실행된다. 이 때 동적으로 메서드 정보가 넘어간다.
- JDK 동적 프록시로 만들어진 프록시는 com.sun.proxy로 표시된다.
- 프록시 몸뚱아리에 어떤 클래스를 기본으로 할지 전달해준다.. 따라서 실제 객체와 바꿔치기를 할 수 있다.
- CGLIB 동적 프록시 정리
- CGLIB 라이브러리를 이용해 바이트 코드를 조작해서 동적으로 프록시를 생성한다.
- CGLIB의 Enhancer를 통해서 만든다.
- 구체 클래스 정보(SuperClass)와 MethodInterceptor(공통 프록시 몸통 정보)를 넘겨서 만든다.
- MethodInterceptor의 구현체가 공통 프록시 몸통 정보가 된다.
- CGLIB는 바이트 코드를 조작해서 실제 구체 클래스를 상속받은 클래스로 프록시 객체를 만든다.
- CGLIB 동적 프록시 객체를 호출하면 MethodInterceptor.intercept()가 바로 실행된다. 이 때 동적으로 메서드 정보가 넘어간다.
- CGLIB로 만들어진 프록시는 EnhancerByCGLIB라는 것이 붙는 것을 알 수 있다.
- CGLIB에 SuperClass를 지정해서 어떤 것을 상속받아 구현할지 알려준다. 따라서 실제 객체와 바꿔치기가 가능하다.
현재의 한계점.
인터페이스가 있는 곳에는 JDK 동적 프록시로 생성해주고, 없는 곳은 CGLIB로 동적 프록시를 생성해줘야한다. 그렇다면 만들고 나서 의존관계 주입이 2배가 된다는 이야기다. 또한 MethodInterceptor와 InvocationHandler를 각각 2개 만들어서 또 등록해줘야한다.
이런 반복적인 부분을 스프링은 프록시 팩토리라는 것으로 추상화 시켜 도움을 준다. 다음 포스팅에서는 프록시 팩토리에 대해 정리를 하고자 한다.
'Spring > Spring' 카테고리의 다른 글
스프링 AOP : Bean PostProcessor를 이용한 의존관계 주입 (0) | 2022.01.29 |
---|---|
스프링 AOP : ProxyFactory를 이용한 동적 프록시 처리 (0) | 2022.01.29 |
스프링 AOP : 프록시 패턴, 데코레이터 패턴 도입 (0) | 2022.01.28 |
스프링 AOP : 템플릿 메서드 패턴, 전략 패턴, 템플릿 콜백 패턴 적용 (0) | 2022.01.26 |
스프링의 이해 : 동시성 문제와 ThreadLocal 처리 (0) | 2022.01.25 |