[Spring AOP] 동적 프록시
프록시 패턴은 클라이언트가 원본 객체를 이용하는 것이 아닌 대리자를 거쳐서 원본 객체에 접근하는 디자인 패턴이다. 하지만 해당 패턴은 적용하려는 원본 객체의 수만큼 프록시 클래스를 생성해야 한다는 단점이 존재한다.
이러한 단점을 해결하기 위해 JVM은 컴파일 시점이 아닌 런타임에 개발자 대신 프록시 클래스를 생성해주는 기능을 제공하는데 이를 동적 프록시라고 한다.
프록시는 크게 두 가지 유형으로 나눌 수 있다.
인터페이스 기반 프록시와 클래스 기반 프록시가 있는데 동적 프록시의 종류와 함께 이를 살펴보도록 하겠다.
https://hbb-devlog.tistory.com/83
JDK 동적 프록시
JDK 동적 프록시 기능은 java.lang.reflect.Proxy 패키지에서 제공한다.
해당 기능은 인터페이스를 기반으로 프록시를 생성하기 때문에 인터페이스가 필수이다.
Proxy 클래스 내부에 newProxyInstance() 메서드를 통해 프록시 인스턴스를 생성할 수 있다.
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {
Objects.requireNonNull(h);
final Class<?> caller = System.getSecurityManager() == null
? null
: Reflection.getCallerClass();
...
return newProxyInstance(caller, cons, h);
}
newProxyInstance() 메서드는 3가지의 매개변수를 받는다.
- 클래스 로더
- 프록시 클래스를 생성 할 클래스 로더
- Proxy 객체가 구현할 Interface의 클래스 로더
- 프록시 클래스를 생성 할 클래스 로더
- 타겟의 인터페이스
- 프록시 클래스가 구현하고자 하는 인터페이스 목록
- 생성될 Proxy 객체가 구현할 인터페이스를 정의
- 프록시 클래스가 구현하고자 하는 인터페이스 목록
- 타겟의 정보가 포함된 핸들러
- 프록시의 메서드가 호출되었을 때 실행되는 핸들러 메서드
위에서 마지막 매개변수인 핸들러, 즉 JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하고 해당 구현체를 전달한다.
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
- Object proxy : 프록시 자신
- Method method : 호출한 메서드
- Object[] args : 메서드를 호출할 때 전달되는 인수 배열
내부에는 invoke() 메서드 하나 존재하는데 이는 동적 프록시의 메서드가 호출될 때 이를 낚아채서 대신 실행되는 메서드이다.
@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;
}
}
void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
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());
}
TimeInvocationHandler - TimeProxy 실행
AImpl - A 호출
TimeInvocationHandler - TimeProxy 종료 resultTime=0
JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy1
실행 흐름을 살펴보면 아래와 같다.
- Proxy.newProxyInstance()를 통해 동적 프록시 생성
- 클라이언트는 JDK 동적 프록시의 call() 실행
- JDK 동적 프록시는 InvocationHandler.invoke()를 호출
- TimeInvocationHandler.invoke()
- TimeInvocationHandler 가 내부 로직을 수행하고, method.invoke(target, args)를 호출해 서 target인 실제 객체(AImpl)를 호출
- AImpl 인스턴스의 call() 실행
- AImpl 인스턴스의 call() 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아오고, 시간 로그를 출력하고 결과를 반환
따라서 프록시 패턴과는 다르게 프록시 적용 대상만큼의 프록시 객체를 생성할 필요가 없고, 필요한 InvocationHandler만 정의하면 런타임 시점에 프록시 객체를 자동으로 생성해준다.
하지만 이는 인터페이스가 필수이므로, 구체클래스만 존재하는 경우 프록시를 적용할 수 없다는 단점이 있다.
CGLIB
CGLIB란 바이트코드를 조작하여 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
해당 라이브러리를 사용하면 JDK 동적 프록시와는 다르게 구체 클래스만 있어도 동적 프록시를 생성할 수 있다.
스프링을 사용한다면 별도로 라이브러리를 추가하지 않아도 CGLIB 기능을 사용할 수 있다.
추가로 CGLIB는 인터페이스 기반 동적 프록시, 클래스 기반 동적 프록시 2가지를 모두 지원한다.
위 JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler 를 제공한 것처럼, CGLIB는 MethodInterceptor 를 제공한다.
public interface MethodInterceptor extends Callback {
Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
- obj : CGLIB가 적용된 객체
- method : 호출된 메서드
- args : 메서드를 호출하면서 전달된 인수
- proxy : 메서드 호출에 사용
@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 proxy) throws Throwable {
log.info("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = proxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
@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();
}
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=10
실행 흐름을 살펴보면 다음과 같다. (ConcreteService는 인터페이스가 없는 구체 클래스)
- Enhancer : CGLIB는 Enhancer를 사용하여 프록시를 생성
- enhancer.setSuperclass(ConcreteService.class) : 상속받을 구체 클래스 지정
- enhancer.setCallback(new TimeMethodInterceptor(target)) : 프록시에 적용할 실행 로직을 할당
- enhancer.create() : 위에서 지정한 구체 클래스를 상속받아 프록시를 생성한다.
- 동적 프록시의 proxy.call() 호출
- CGLIB는 MethodInterceptor.intercept()를 호출
- TimeMethodInterceptor 가 내부 로직을 수행하고, proxy.invoke(target, args)를 호출해 서 target인 실제 객체(ConcreteService)를 호출
- ConcreteService 인스턴스의 call() 실행
- ConcreteService 인스턴스의 call() 실행이 끝나면 TimeMethodInterceptor 로 응답이 돌아오고, 시간 로그를 출력하고 결과를 반환
따라서 CGLIB를 사용하면 인터페이스가 없이 구체 클래스만으로 동적 프록시 생성이 가능하다.
하지만 상속을 사용하므로 다음 제약이 존재한다.
- CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다. CGLIB에서는 예외가 발생
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
- CGLIB에서는 프록시 로직이 동작하지 않는다.
참고
스프링 핵심 원리 - 고급편 강의 | 김영한 - 인프런
김영한 | 스프링의 핵심 원리와 고급 기술들을 깊이있게 학습하고, 스프링을 자신있게 사용할 수 있습니다., 핵심 디자인 패턴, 쓰레드 로컬, 스프링 AOP스프링의 3가지 핵심 고급 개념 이해하기
www.inflearn.com