프록시란?
프록시는 서버와 클라이언트 사이에서 통신을 대신해주는 역할을 하는 객체이다.
자바에서는 이 프록시를 통해서 JDK 동적 프록시
와 CGLIB 프록시
를 지원해서 프록시를 직접 생성하는 것이 아닌, 프록시 객체를 런타임 환경에서 자동으로 만들 수 있다.
JDK 동적 프록시는 인터페이스의 구현체일 경우 생성되며, CGLIB 동적 프록시의 경우 인터페이스가 없는 구현체 클래스일 경우에 생성 된다.
스프링에서는 AOP 적용을 인터페이스 구현없이 할 수 있도록 기본 방식으로 CGLIB를 채택하여 사용하고 있다. (레거시는 JDK 동적 프록시 사용)
JDK 동적 프록시와 CGLIB 프록시에 대해 알아보자.
프록시에 대한 개념은 하단의 포스팅을 참고하자.
https://sasca37.tistory.com/278
JDK 동적 프록시
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. (인터페이스가 필수)
JDK 프록시는 InvocationHandler
인터페이스 구현을 통해 만들 수 있다. 예제 코드를 통해 JDK 동적 프록시를 알아보자.
JDK 동적 프록시 - 예제
JDK 동적 프록시 - InvocationHandler 인터페이스
package java.lang.reflect;
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
- 자바 리플렉션 (java.lang.reflect)에서 인터페이스를 제공한다.
Object
는 프록시 자신,Method
는 호출한 메서드,args
는 메서드를 호출할 때 전달한 파라미터 정보를 갖고 있다.
TimeInvocationHandler 클래스 - InvocationHandler 구현체
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
@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;
}
}
Object target
이 동적 프록시를 호출할 대상이며, 리플렉션을 사용하여 target 인스턴스의 메서드를 실행해보자.
AInterface 인터페이스, AImpl 구현체
public interface AInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface{
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
- 테스트를 위한 인터페이스와 구현체를 생성하자.
JdkDynamicProxyTest
import hello.proxy.jdkdynamic.code.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Proxy;
@Slf4j
public class JdkDynamicProxyTest {
@Test
void dynamicA() {
AInterface target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target); // AImpl 이 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());
}
}
java.lang.reflect.Proxy
를 통해 동적프록시를 생성한다. (클래스로더 정보, 대상 인터페이스, 핸들러 정보)Proxy
는 Object 반환이므로 형변환을 통해 인터페이스로 만든 후 선언한call()
메서드를 호출할 수 있다.
JdkDynamicProxyTest 실행결과
11:23:52.923 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 실행
11:23:52.924 [Test worker] INFO hello.proxy.jdkdynamic.code.AImpl - A 호출
11:23:52.924 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 종료 resultTime=0
11:23:52.925 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
11:23:52.925 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy12
- 실행 결과를 보면 프록시의 클래스가
com.sun.proxy.$ProxyX
로 프록시가 정상 수행된 것을 확인할 수 있다.
JDK 동적 프록시 실행 순서
- client가 JDK 동적 프록시의
call()
메서드를 실행한다. - JDK 동적 프록시는
InvocationHandle.invoke()
를 호출하여, 구현체인timeInvocationHandler.invoke()
를 실행한다.
CGLIB 동적 프록시
CGLIB는 Code Generator Library라는 의미로 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다. JDK 동적 프록시와 다르게 인터페이스 없이 구체 클래스만으로도 동적 프록시를 만들 수 있다.
CGLIB는 원래 외부 라이브러리지만, 스프링 프레임워크가 내부에 포함되어 있어서 스프링을 사용한다면, 별도의 설정 없이 사용할 수 있다.
MethodInterceptor - CGLIB 제공
package org.springframework.cglib.proxy;
import java.lang.reflect.Method;
public interface MethodInterceptor extends Callback {
Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;
}
- JDK 동적 프록시가 InvocationHandler를 제공하는 것처럼 CGLIB도 MethodInterceptor를 제공한다.
TimeMethodInterceptor - MethodInterceptor 구현체
import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
@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("TimeProxy 실행");
long startTime = System.currentTimeMillis();
Object result = methodProxy.invoke(target, args);
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeProxy 종료 resultTime={}", resultTime);
return result;
}
}
- JDK 동적 프록시처럼 메서드를
invoke
하여 프록시가 실제 호출할 대상에 접근한다. Method
와MethodProxy
가 있는데 두 개 모두 동일한 기능을 하나MethodProxy
가 CGLIB 성능 상의 이점이 있어 권장한다고 한다.
CglibTest - CGLIB 적용 테스트
@Slf4j
public class CglibTest {
@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();
}
static class ConcreteService { // 인터페이스가 없는 구체 클래스
public void call() {
log.info("ConcreteService 호출");
}
}
}
- CGLIB는
Enhancer
를 사용해서 구체 클래스 상속과 실행로직(콜백)을 할당 받아 프록시를 생성한다.
CglibTest - 실행결과
16:56:33.655 [Test worker] INFO hello.proxy.cglib.CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
16:56:33.658 [Test worker] INFO hello.proxy.cglib.CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
16:56:33.658 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 실행
16:56:33.663 [Test worker] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
16:56:33.664 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 종료 resultTime=6
- CGLIB를 통해 동적 프록시가 생성되어
대상클래스$$EnhancerByCGLIB$$임의코드
형태로 생성된다.
CGLIB 주의 사항
CGLIB 동적 프록시 방식은 클래스 기반으로 상속 받아 사용하기 때문에 몇 가지 주의할 점이 있다.
- 부모 클래스의 생성자 기본 생성자 체크 (자식 클래스를 동적으로 생성)
- 클래스 또는 메서드에
final
키워드가 붙지 않았는 지 확인 (클래스는 상속 불가, 메서드는 오버라이딩 불가)
본 포스팅은 인프런 - 스프링 핵심 원리 - 고급편을 참고하여 작성하였습니다.