1. 프록시 팩토리 (Proxy Factory)
프록시 생성에 있어 인터페이스 유무에 따라 적용되는 기술이 다르다. 그 때문에 각 기술에 맞춰 InvocationHandler 와 MethodInterceptor 를 중복으로 만들어야 하는가? 하는 문제가 있다. 같은 코드일텐데 공통으로 사용할 수는 없을까?
- JDK 동적 프록시 (링크)
- 인터페이스와 구현 클래스가 있는 경우
- InvocationHandler 사용
- CGLIB (링크)
- 구체 클래스만 존재하는 경우
- MethodInterceptor 사용
스프링은 이렇게 유사한 기술이 있을 때 통합해서 일관성있게, 편리하게 사용할 수 있도록 추상화된 기술을 제공한다. 스프링은 동적 프록시를 통합해서 편리하게 만들어주는 프록시 팩토리 (ProxyFactory) 기능을 제공한다. 프록시 팩토리 하나로 인터페이스가 있으면 JDK 동적 프록시를 사용하고, 인터페이스 없이 구체클래스만 존재하면 CGLIB 를 사용할수 있다.
이때, InvocationHandler 와 MethodInterceptor 를 따로 만들어야 했던 문제를 해결하기위해 스프링은 Advice 라는 개념을 도입했다. 핸들러와 인터셉터를 신경쓰지 않고, Advice 만 만들면 된다.
프록시 팩토리를 사용하면 스프링이 Advice 를 호출하는 AdviceInvocationHandler , AdviceMethodInterceptor 를 내부적으로 사용한다.
2. 예제코드
1. 인터페이스 와 구현 클래스가 존재하는 경우 (JDK 동적 프록시) : CommandService, CommandServiceImpl
2. 구체 클래스만 존재하는 경우 (CGLIB) : ConcreteService
/** 1. 인터페이스와 구현 클래스가 존재하는 경우 (JDK 동적 프록시) */
// 인터페이스 CommandService
public interface CommandService {
String find();
String modify();
}
// 구현 클래스 CommandServiceImpl
@Slf4j
public class CommandServiceImpl implements CommandService {
@Override
public String find() {
log.info("find() 호출");
return "find() complete";
}
@Override
public String modify() {
log.info("modify() 호출");
return "modify() complete";
}
}
/** 구체 클래스만 존재하는 경우 (CGLIB) */
@Slf4j
public class ConcreteService {
public String call() {
log.info("call() 호출");
return "call() complete";
}
}
MethodInterceptor 를 구현하여 TimeCheckAdvice 작성 : JDK 동적 프록시의 InvocationHandler 와 CGLIB 의 MethodInterceptor 를 대신하여 Advice 를 작성
이때, 패키지명을 유의해야한다.
- 스프링이 제공하는 Advice 개념의 인터셉터
import org.aopalliance.intercept.MethodInterceptor - CGLIB
import org.springframework.cglib.proxy.MethodInterceptor;
// 스프링이 제공하는 MethodInterceptor 를 구현하므로 패키지명 유의!
import org.aopalliance.intercept.MethodInterceptor
@Slf4j
public class TimeCheckAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
log.info("TimeCheckProxy 실행");
long startTime = System.currentTimeMillis();
// target 클래스 호출하고 그 결과를 취득
// target 클래스 정보는 ? MethodInvocation invocation 에 들어있음
// 프록시 팩토리로 프록시 생성 단계에서 target 정보를 파라미터로 전달하므로
Object result = invocation.proceed(); // 로직
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeCheckProxy 종료 resultTime = {}", resultTime);
return result;
}
}
어드바이스 확인 (테스트 코드)
import static org.junit.jupiter.api.Assertions.*;
@Slf4j
public class ProxyFactoryTest {
@Test
@DisplayName("인터페이스가 있는 경우 : JDK 동적 프록시")
void interfaceProxy() {
CommandService target = new CommandServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeCheckAdvice());
CommandService proxy = (CommandService) proxyFactory.getProxy();
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
proxy.find();
// 인터페이스 있는경우, AopProxy 이며, JDK 동적 프록시
assertTrue(AopUtils.isAopProxy(proxy));
assertTrue(AopUtils.isJdkDynamicProxy(proxy));
assertFalse(AopUtils.isCglibProxy(proxy));
}
@Test
@DisplayName("구체 클래스만 있는 경우: CGLIB")
void concreteProxy() {
ConcreteService target = new ConcreteService();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeCheckAdvice());
ConcreteService proxy = (ConcreteService) proxyFactory.getProxy();
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
proxy.call();
// 구체 클래스만 있으면 AopProxy 이며, CGLIB
assertTrue(AopUtils.isAopProxy(proxy));
assertFalse(AopUtils.isJdkDynamicProxy(proxy));
assertTrue(AopUtils.isCglibProxy(proxy));
}
@Test
@DisplayName("proxyTargetClass(true) 설정시 인터페이스가 있어도 CGLIB, 클래스 기반 프록시 사용")
void proxyTargetClass() {
CommandService target = new CommandServiceImpl();
ProxyFactory proxyFactory = new ProxyFactory(target);
proxyFactory.addAdvice(new TimeCheckAdvice());
// 스프링부트는 항상 true 로 설정해서 항상 CGLIB 로 생성한다.
proxyFactory.setProxyTargetClass(true); // 중요
CommandService proxy = (CommandService) proxyFactory.getProxy();
log.info("targetClass = {}", target.getClass());
log.info("proxyClass = {}", proxy.getClass());
proxy.modify();
// setProxyTargetClass(true) 이면 인터페이스도 CGLIB 사용
assertTrue(AopUtils.isAopProxy(proxy));
assertFalse(AopUtils.isJdkDynamicProxy(proxy));
assertTrue(AopUtils.isCglibProxy(proxy));
}
}
실행결과확인
1. interfaceProxy() : "인터페이스가 있는 경우 : JDK 동적 프록시"
ProxyFactoryTest - targetClass = class hello.proxy.proxyfactorybasic.code.CommandServiceImpl
ProxyFactoryTest - proxyClass = class com.sun.proxy.$Proxy10
TimeCheckAdvice - TimeCheckProxy 실행
CommandServiceImpl - find() 호출
TimeCheckAdvice - TimeCheckProxy 종료 resultTime = 0
2. concreteProxy() : "구체 클래스만 있는 경우: CGLIB"
ProxyFactoryTest - targetClass = class hello.proxy.proxyfactorybasic.code.ConcreteService
ProxyFactoryTest - proxyClass = class hello.proxy.proxyfactorybasic.code.ConcreteService$$EnhancerBySpringCGLIB$$88fa8dd6
TimeCheckAdvice - TimeCheckProxy 실행
ConcreteService - call() 호출
TimeCheckAdvice - TimeCheckProxy 종료 resultTime = 61
3. proxyTargetClass() : "proxyTargetClass(true) 설정시 인터페이스가 있어도 CGLIB, 클래스 기반 프록시 사용"
ProxyFactoryTest - targetClass = class hello.proxy.proxyfactorybasic.code.CommandServiceImpl
ProxyFactoryTest - proxyClass = class hello.proxy.proxyfactorybasic.code.CommandServiceImpl$$EnhancerBySpringCGLIB$$df682fcf
TimeCheckAdvice - TimeCheckProxy 실행
CommandServiceImpl - modify() 호출
TimeCheckAdvice - TimeCheckProxy 종료 resultTime = 41
3. 프록시 팩토리의 동작
- 인터페이스가 있는 경우 : JDK 동적 프록시, 인터페이스 기반 프록시
- 인터페이스가 없는 경우 : CGLIB, 구체 클래스 기반 프록시
- proxyTargetClass=ture : CGLIB, 구체 클래스 기반 프록시, 인터페이스 존재 무관
4. 정리
프록시 팩토리의 서비스 추상화로 JDK 동적 프록시, CGLIB 에 의존하지 않고 편리하게 동적 프록시를 생성할 수 있다. 프록시의 부가기능 로직도 핸들러와 인터셉터 대신에 Advice 하나만 만들어 사용할 수 있다.
스프링이 내부에서 JDK 동적 프록시인 경우 InvocationHandler 가, CGLIB 인 경우 MethodInterceptor 가 Advice 를 호출하도록 해두었기 때문이다.