지난 포스팅까지 해서 JDK 동적 프록시와 CGLIB을 활용한 동적 프록시 생성 방법에 대해서 알아보았다.
지난 포스팅에서 드러난 문제가 몇가지 있었는데,
그런데 JDK 동적 프록시와 CGLIB은 유사한 형태를 띄고 있고, 그 기능도 유사하다.
스프링은 이런 유사한 기술들에 대해 편리하게 사용 가능하도록 추상화된 기술을 제공한다.
그리고 그 기술이 ProxyFactory이다.
프록시 팩토리는 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 구체 클래스만 있으면 CGLIB을 사용한다.
스프링에서는 이런 문제를 해결하기 위해 Advice라는 것을 지원한다.
결과적으로 InvocationHandler나 MethodInterceptor가 Advice를 호출하게 되기 때문에, 개발자는 두 경우를 따로 신경쓸 필요 없이 Advice를 만들면 된다.
프록시 팩토리를 사용하면 내부에서 Advice 를 호출하는 전용 InvocationHandler, MethodInterceptor를 사용하기 때문이다.
그럼 코드로 확인해보자.
Advice는 프록시에 적용하는 부가 기능으로 InvocationHandler와 MethodInterceptor와 유사하며 이 둘을 추상화한 개념이다. 따라서 프록시 팩토리에서는 둘 대신에 Advice를 사용하면 된다.
import lombok.extern.slf4j.Slf4j;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
@Slf4j
public class TimeAdvice implements MethodInterceptor {
// 타겟의 정보는 MethodInvocation안에 이미 들어가 있음
// 프록시 팩토리를 생성하는 시점에 타겟 정보를 넘겨야함
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
// Object result = method.invoke(target, args);
Object result = invocation.proceed();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy종료 resultTime={}",resultTime);
return result;
}
}
@Test
@DisplayName("인터페이스가 있으면 JDK 동적 프록시 사용")
void interfaceProxy() {
ServiceImpl target = new ServiceImpl();
// 프록시 팩토리를 생성하는 시점에 타겟을 인수로 넘김
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass={}",target.getClass());
log.info("proxyClass={}",proxy.getClass());
proxy.save();
// AopUtils는 프록시 팩토리를 이용해 프록시를 생성했을 경우에만 사용 가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isTrue();
assertThat(AopUtils.isCglibProxy(proxy)).isFalse();
}
테스트 결과를 확인해보자.
인터페이스의 경우
검증 결과도 참으로 나오고, 로그도 AOP 동적 프록시가 찍히는 것을 확인할 수 있다.
구현 클래스의 경우
@Test
@DisplayName("ProxyTargetClass를 사용하면 인터페이스가 있어도 CGLIB을 사용하고, 구현 클래스 기반 프록시 사용")
void proxyTargetClass() {
ServiceImpl target = new ServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
// 프록시의 타겟 클래스를 기반으로 프록시 팩토리 생성
proxyFactory.setProxyTargetClass(true);
proxyFactory.addAdvice(new TimeAdvice());
ServiceInterface proxy = (ServiceInterface) proxyFactory.getProxy();
log.info("targetClass={}",target.getClass());
log.info("proxyClass={}",proxy.getClass());
proxy.save();
// AopUtils는 프록시 팩토리를 이용해 프록시를 생성했을 경우에만 사용 가능
assertThat(AopUtils.isAopProxy(proxy)).isTrue();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).isFalse();
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
}
이처럼 프록시 팩토리를 활용하면 JDK 동적 프록시나 CGLIB에 관계 없이 동적 프록시를 편리하게 생성할 수 있다.
스프링에서는 Advice로 추상화를 제공하고 있는데, 프록시 팩토리 내부에서 JDK 동적 프록시의 경우 InvocationHandler를, CGLIB의 경우 MethodInterceptor를 호출하도록 개발하였다.
덕분에 우리는 Advice라는 추상화를 활용해 프록시 로직을 편하게 적용할 수 있게 되었다.
출처 : 김영한 - 스프링 핵심 원리 고급편