들어가며
기존에 정리했던 프록시 패턴(https://brightstarit.tistory.com/48)을 통해 기존 코드를 변경하지 않고 핵심 기능과 부가 기능을 나누어서 개발할 수 있었다.
하지만, 프록시 패턴을 사용하면 대상 클래스의 수만큼 프록시 클래스를 만들어야 한다는 단점이 있었다.
클래스를 계속 생성하면 관리 포인트가 늘어나서 유지보수하기 쉽지 않다. 예를 들어 적용 대상이 100개면 프록시 클래스도 100개를 만들어야 되는 것이다.
그런데 프록시 클래스의 기본 코드와 흐름은 거의 같고, 프록시를 어떤 대상에 적용하는가 정도만 차이가 있다.
이 문제를 해결하는 것이 바로 동적 프록시 기술이다. 동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다.
이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다.
이런 동적 프록시 기술로는 자바에서 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술이 있다.
JDK 동적 프록시
- 자바에서 기본적으로 제공하는 동적 프록시 기술로 리플렉션(https://brightstarit.tistory.com/50)을 사용한다.
- 리플렉션을 사용하기 때문에 성능상 이슈가 있다.
- 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수다.
public interface AInterface {
String call();
}
public interface BInterface {
String call();
}
@Slf4j
public class AImpl implements AInterface{
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
@Slf4j
public class BImpl implements BInterface{
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
JDK 동적 프록시에 적용할 로직은 InvocationHandler
인터페이스를 구현해서 작성하면 된다.
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
제공되는 파라미터는 다음과 같다.
Object Proxy
: 프록시 자신Method method
: 호출한 메서드Object[] args
: 메서드를 호출할 때 전달한 인수
TimeInvocationHandler.java
@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;
}
}
TimeInvoationHander
는InvocationHandler
인터페이스를 구현한다.Object target
: 동적 프록시가 호출할 대상method.invoke(target, args)
: 리플렉션을 사용해서target
인스턴스의 메서드를 실행한다.args
는 메서드 호출 시 넘겨줄 인수이다.
JdkDynamicProxyTest.java
@Slf4j
public class JdkDynamicProxyTest {
@Test
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());
}
@Test
void dynamicB() {
BInterface target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
BInterface proxy = (BInterface) Proxy.newProxyInstance(BInterface.class.getClassLoader(),
new Class[]{BInterface.class}, handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}
DynamicA 실행 결과
21:18:49.153 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 실행
21:18:49.156 [Test worker] INFO hello.proxy.jdkdynamic.code.AImpl - A 호출
21:18:49.156 [Test worker] INFO hello.proxy.jdkdynamic.code.TimeInvocationHandler - TimeProxy 종료 resultTime=0
21:18:49.157 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - targetClass=class hello.proxy.jdkdynamic.code.AImpl
21:18:49.157 [Test worker] INFO hello.proxy.jdkdynamic.JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy12
new TomeInvocationHandler(target)
: 동적 프록시에 적용할 핸들러 로직이다.- 동적 프록시는
java.lang.reflect.Proxy
를 통해서 생성할 수 있다. - 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다. 그러면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.
proxyClass=class co m.sun.proxy.$Proxy12
이 부분이 동적으로 생성된 프록시 클래스 정보이다.
실행 순서
- 클라이언트는 JDK 동적 프록시의
call()
을 실행한다. - JDK 동적 프록시는
InvocationHandler.invoke()
를 호출한다. TimeInvocationHandler가 구현체로 있으므로TimeInvocationHandler.invoke()
가 호출된다. - TimeInvocationHandler가 내부 로직을 수행하고,
method.invoke(target, args)
를 호출해서 target인 실제 객체(AImpl)를 호출한다. - AImpl 인스턴스의
call()
이 실행된다. - AImpl 인스턴스의
call()
의 실행이 끝나면 TimeInvocationHandler로 응답이 돌아온다. 시간 로그를 출력하고 결과를 반환한다.
CGLIB
- 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다.
- 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
- 원래는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 따라서 스프링을 사용한다면 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.
구체 클래스 - ConcreteService.java
@Slf4j
public class ConcreteService {
public void call() {
log.info("ConcreteService 호출");
}
}
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 : 메서드 호출에 사용
TimeMethodInterceptor.java
@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 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;
}
}
TimeMethodInterceptor
는MethodInterceptor
인터페이스를 구현해서 CGLIB 프록시의 실행 로직을 정의한다.proxy.invoke(target, args)
: 실제 대상을 동적으로 호출한다.method
를 사용해도 좋지만, CGLIB은 성능상MethodProxy proxy
를 사용하는 것을 권장한다.
CglibTest.java
@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();
}
}
01:23:34.871 [Test worker] INFO hello.proxy.cglib.CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
01:23:34.876 [Test worker] INFO hello.proxy.cglib.CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
01:23:34.877 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 실행
01:23:34.887 [Test worker] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
01:23:34.887 [Test worker] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 종료 resultTime=10
- Enhancer : CGLIB는 Enhancer를 사용해서 프록시를 생성한다.
enhancer.setSuperclass(CocreteService.class)
: CGLIB는 구체 클래스를 상속받아서 프록시를 생성할 수 있다. 어떤 구체 클래스를 상속받을지 지정한다.enhancer.setCallback(new TimeMethodInterceptor(target))
: 프록시에 적용할 실행 로직을 할당한다.enhancer.create()
: 프록시를 생성한다. 앞서 설정한enhancer.setSuperclass(CocreteService.class)
에서 지정한 클래스를 상속받아서 프록시가 만들어진다.
JDK 동적 프록시는 인터페이스를 구현(implement)해서 프록시를 만든다. CGLIB은 구체 클래스를 상속(extends)해서 프록시를 만든다.
CGLIB가 동적으로 생성하는 클래스 이름은 다음과 같은 규칙으로 생성된다.
- 대상클래스$$EnhancerByCGLIB$$임의코드
CGLIB 제약
클래스 기반 프록시는 상속을 사용하기 때문에 몇가지 제약이 있다.
- 부모 클래스의 생성자를 체크해야 한다.
- CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
- 클래스에 final 키워드가 붙으면 상속이 불가능하다.
- CGLIB에서는 예외가 발생한다.
- 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다.
- CGLIB에서는 프록시 로직이 동작하지 않는다.
참고 자료 :
'백엔드 > Spring' 카테고리의 다른 글
레이어간 의존 관계 문제 (0) | 2024.07.29 |
---|---|
[Spring Cloud] Spring Cloud OpenFeign (0) | 2023.05.22 |
[Spring] Spring Boot Actuator (0) | 2023.05.18 |
[Spring] 스프링(Spring) (0) | 2023.04.28 |
[Spring] POJO(Plain Old Java Object) (0) | 2023.04.27 |