프록시 팩토리
인터페이스가 있는 경우에는 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB을 작용하기 위해 스프링이 추상화된 편리한 인터페이스를 제공한다. 즉 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리(Proxy Factory)라는 기능을 제공한다.
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있다면 CGLIB을 사용한다. 또한 이 설정을 변경할 수도 있다.
Q: 두 기술을 함께 사용할 때 부가 기능을 적용하기 위해 JDK 동적 프록시가 제공하는 InvocationHandler와 CGLIB가 제공하는 MethodInterceptor를 각각 중복으로 따로 만들어야 할까?
스프링은 이 문제를 해결하기 위해 부가 기능을 적용할 때 Advice 라는 새로운 개념을 도입했다. 개발자는 InvocationHandler 나 MethodInterceptor 를 신경쓰지 않고, Advice 만 만들면 된다.
결과적으로 InvocationHandler 나 MethodInterceptor 는 Advice 를 호출하게 된다.
프록시 팩토리를 사용하면 Advice를 호출하는 전용 InvocationHandler , MethodInterceptor 를 내부에서 사용한다.
![](https://blog.kakaocdn.net/dn/Y7XTD/btrp7Hu2p87/pUnfbM3GjDTcsTtit9qTi1/img.png)
개발자가 Advice를 구현하면 스프링이 adviceInvocationHandler 또는 adviceMethodInterceptor를 사용하여 개발자가 구현한 Advice를 호출하도록 해준다.
런터임 시에는 아래와 같다.
Q: 특정 조건에 맞을 때 프록시 로직을 적용하는 기능도 공통으로 제공되었으면?
앞서 특정 메서드 이름의 조건에 맞을 때만 프록시 부가 기능이 적용되는 코드를 직접 만들었다. 스프링은 Pointcut 이라는 개념을 도입해서 이 문제를 일관성 있게 해결한다.
스프링이 제공하는 프록시 팩토리를 예제 코드로 살펴보자.
Advice 만들기
Advice 는 프록시에 적용하는 부가 기능 로직이다. 이것은 JDK 동적 프록시가 제공하는 InvocationHandler 와 CGLIB가 제공하는 MethodInterceptor 의 개념과 유사하다. 둘을 개념적으로 추상화한 것이다. 프록시 패턴을 사용한다면 둘 대신에 Advice를 사용하면 된다.
Advice를 만드는 가장 기본적인 방법은 스프링이 제공하는 MethodInterceptor를 구현하는 것이다. (MethodInterceptor는 Interceptor를 상속하고 Interceptor는 Advice를 상속한다.)
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;//aopalliance 패키지의 MethodInterceptor 사용
import org.aopalliance.intercept.MethodInvocation;
@Slf4j
public class TimeAdvice implements MethodInterceptor {
//MethodInvocation에 target 클래스 정보가 담겨있다.
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("Time Proxy 실행");
long startTime = System.currentTimeMillis();
Object result = invocation.proceed();//invocation.proceed()를 호출하면 알아서 target을 찾아 호출해준다.
long endTime = System.currentTimeMillis();
log.info("TimeProxy 종료 resultTime = {}", endTime-startTime);
return result;
}
}
테스트에 사용할 서비스의 인터페이스와 구현체는 아래와 같다.
1. 인터페이스가 있는 경우를 테스트하기 위한 ServiceInterface와 그 구현체인 ServiceImpl
public interface ServiceInterface {
void save();
void find();
}
@Slf4j
public class ServiceImpl implements ServiceInterface {
@Override
public void save() {
log.info("save 호출");
}
@Override
public void find() {
log.info("find 호출");
}
}
2. 인터페이스가 없고 구체 클래스만 있는 경우를 테스트하기 위한 ConcreteService
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}
이제 테스트해보자.
1. 인터페이스가 있는 경우를 테스트:
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);//이때 proxyFactory에 target의 정보가 넘어감
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
proxy.save();
// 프록시 팩토리로 프록시가 잘 적용되었는지 확인
assertThat(AopUtils.isAopProxy(proxy)).isTrue(); //OK
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue(); //OK
assertThat(AopUtils.isCglibProxy(proxy)).isFalse(); //OK
}
실행결과:
targetClass = class hello.proxy.common.ServiceImpl
proxyClass = class com.sun.proxy.$Proxy13 //JDK 동적 프록시
Time Proxy 실행
save 호출
TimeProxy 종료 resultTime = 0
2. 인터페이스가 없는 경우 테스트 :
@Test
@DisplayName("구체 클래스만 있으면 CGLIB 사용")
void noInterfaceProxy() {
ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);//이때 proxyFactory에 target의 정보가 넘어감
proxyFactory.addAdvice(new TimeAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
proxy.call();
assertThat(AopUtils.isAopProxy(proxy)).isTrue(); //OK
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse(); //OK
assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); //OK
}
실행결과:
targetClass = class hello.proxy.common.ConcreteService
proxyClass = class hello.proxy.common.ConcreteService$$EnhancerBySpringCGLIB$$3f3ec610
Time Proxy 실행
ConcreteService 호출
TimeProxy 종료 resultTime = 11
3. 인터페이스를 가지고 있지만 무조건 구체 클래스 기반으로 프록시 사용하기 ( setProxyTargetClass )
@Test
@DisplayName("ProxyTargetClass 옵션을 사용하면 인터페이스가 있어도 CGLIB를 사용하고, 클래스 기반 프록시 사용")
void proxyTargetClass() {
ServiceInterface target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.setProxyTargetClass(true);//CGLIB를 사용하고, 클래스 기반 프록시 사용
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
proxy.save();
assertThat(AopUtils.isAopProxy(proxy)).isTrue(); //OK
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse(); //OK
assertThat(AopUtils.isCglibProxy(proxy)).isTrue(); //OK
}
실행결과:
targetClass = class hello.proxy.common.ServiceImpl
proxyClass = class hello.proxy.common.ServiceImpl$$EnhancerBySpringCGLIB$$eb04c714 //CGLIB
Time Proxy 실행
save 호출
TimeProxy 종료 resultTime = 10
프록시 팩토리는 proxyTargetClass 라는 옵션을 제공하는데, 이 옵션에 true 값을 넣으면 인터페이스가 있어도 강제로 CGLIB를 사용한다. 그리고 인터페이스가 아닌 클래스 기반의 프록시를 만들어준다.
참고
스프링 부트는 AOP를 적용할 때 기본적으로 proxyTargetClass=true 로 설정해서 사용한다. 따라서 인터페이스가 있어도 항상 CGLIB를 사용해서 구체 클래스를 기반으로 프록시를 생성한다.
김영한님의 인프런 강의와 PDF를 바탕으로 정리하였습니다.
'Spring > spring AOP' 카테고리의 다른 글
스프링 코어 2 - 빈 후처리기 (0) | 2022.01.11 |
---|---|
스프링 코어 2 - 포인트 컷, 어드바이스, 어드바이저 (0) | 2022.01.10 |
스프링 코어2 - CGLIB (0) | 2022.01.10 |
스프링 코어2 - JDK 동적 프록시 (0) | 2022.01.09 |
스프링 코어2 - 리플렉션 (0) | 2022.01.09 |