FireDrago
[Spring] 동적 프록시 (JDK 동적 프록시, CGLIB) 본문
이전 포스팅에서 프록시에 대해 살펴봤다.
그런데 프록시를 적용해보니, 매 객체마다 프록시 객체를 생성해야하는 불편함이 있었다.
이 문제를 해결하기 위한 동적 프록시 기술 JDK 동적 프록시와 CGLIB 에 대해 알아보자
리플렉션
동적 프록시를 이해하기 위해서는 먼저 자바의 리플렉션 기술을 이해해야 한다.
리플렉션 기술은 클래스나 메서드의 메타정보를 획득하고, 코드를 동적으로 호출 할 수 있게 해준다.
메서드를 추상화하여 공통로직을 작성할 수 있게 되는 것이다.
@Test
void reflection2() throws Exception {
// 클래스 메타정보 획득
Class classHello =
Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");
Hello target = new Hello();
// 메서드 이름으로 Method 메타정보 얻는다.
Method methodCallA = classHello.getMethod("callA");
dynamicCall(methodCallA, target);
Method methodCallB = classHello.getMethod("callB");
dynamicCall(methodCallB, target);
}
private void dynamicCall(Method method, Object target) throws Exception {
log.info("start");
// Method를 이용한 공통실행로직
Object result = method.invoke(target);
log.info("result={}", result);
}
리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에서 오류를 잡을 수 없다.
리플렉션 기술을 일반적으로 사용해서는 안되고, 프레임워크 개발이나 일반적인 공통 처리가 필요할때 주의해서 사용하자
JDK 동적 프록시
동적 프록시를 사용하면, 프록시의 로직이 같은데, 적용대상이 다른경우에 동적으로 프록시 객체를 만들어준다.
JDK 동적 프록시는 인터페이스 기반으로 프록시를 동적으로 만들어준다.
프록시 클래스를 수 없이 만들어야 하는 문제도 해결하고,
부가 기능 로직도 하나의 클래스에 모아서 단일책임 원칙(SRP)도 지킬 수 있다.
public interface A {
String call();
}
@Slf4j
public class AImpl implements A{
@Override
public String call() {
log.info("A 호출");
return "a";
}
}
------------------------ 두 인터페이스 ----------------------------
public interface B {
String call();
}
@Slf4j
public class BImpl implements B{
@Override
public String call() {
log.info("B 호출");
return "b";
}
}
인터페이스 A 와 구현체 AImpl 를 정의했다. JDK 동적 프록시를 사용하여 프록시를 사용해보자
@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();
log.info("TimeProxy 종료 resultTime={}", endTime - startTime);
return result;
}
}
JDK 동적 프록시를 사용하기 위해서는 InvocationHandler 인터페이스를 구현해야한다.
이를통해 JDK 동적 프록시에 적용할 공통 로직을 만들 수 있다.
Object target : 동적 프록시가 호출할 실제 객체를 필드로 가진다. 내부적으로 실제 객체를 호출하기 위함이다.
method.invoke(target, args) : 실제 객체의 메서드를 실행한다. args 사용하여 필요한 파라미터들을 넘겨줄 수 있다.
/**
* JDK 동적 프록시를 이용하면 동적으로 프록시 객체를 생성할 수 있기 때문에
* A,B 두 대상에 프록시 적용할때, TimeInvocationHandler 하나로 공통 적용 가능
*/
@Slf4j
public class JdkDynamicProxyTest {
@Test
void dynamicA() {
A target = new AImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
A proxy = (A)Proxy.newProxyInstance(
A.class.getClassLoader(),
new Class[]{A.class},
handler);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
@Test
void dynamicB() {
B target = new BImpl();
TimeInvocationHandler handler = new TimeInvocationHandler(target);
B proxy = (B)Proxy.newProxyInstance(
B.class.getClassLoader(),
new Class[]{B.class},
handler
);
proxy.call();
log.info("targetClass={}", target.getClass());
log.info("proxyClass={}", proxy.getClass());
}
}

jdkdynamic.code.AImpl : jdk 동적 프록시를 이용하여 프록시 객체가 생성된 것을 알 수 있다.
1. 클라이언트는 JDK 동적 프록시의 call() 을 실행한다.
2. JDK 동적 프록시는 InvocationHandler.invoke() 를 호출한다. TimeInvocationHandler 가 구현체로 있으므로 TimeInvocationHandler.invoke() 가 호출된다.
3. TimeInvocationHandler 가 내부 로직을 수행하고, method.invoke(target, args) 를 호출해서 target 인 실제 객체
( AImpl )를 호출한다.
4. AImpl 인스턴스의 call() 이 실행된다.
5. AImpl 인스턴스의 call() 의 실행이 끝나면 TimeInvocationHandler 로 응답이 돌아온다. 시간로그와 결과를 출력

CGLIB
CGLIB은 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어 낼 수 있다.
@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;
}
}
JDK 동적 프록시가 InvocationHandler 인터페이스를 구현했다면,
CGLIB은 MethodInterceptor 인터페이스를 구현한다.
흐름은 두 인터페이스가 비슷한 것을 알 수 있다.
@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 호출");
}
}
}
ConcreteService 는 인터페이스가 없는 구체 클래스이다. 여기에 CGLIB를 사용해서 프록시를 생성했다.
Enhancer : CGLIB는 Enhancer 를 사용해서 프록시를 생성한다.
enhancer.setSuperclass(ConcreteService.class) : CGLIB는 구체 클래스를 상속 받아서 프록시를 생성할 수 있다.
어떤 구체 클래스를 상속 받을지 지정한다.
enhancer.setCallback(new TimeMethodInterceptor(target)) : 프록시에 적용할 실행 로직을 할당한다.
enhancer.create() : 프록시를 생성한다. 앞서 설정에서 지정한 클래스를 상속 받아서 프록시가 만들어진다.

'$$EnhancerByCGLIB$$25d6b0e3' CGLIB 으로 만들어진 프록시 객체임을 알 수 있다.

'프로그래밍 > Spring' 카테고리의 다른 글
| [Spring] 스프링 AOP 개념 (0) | 2024.05.17 |
|---|---|
| [Spring] 스프링이 지원하는 프록시 (0) | 2024.05.15 |
| [Spring] 동시성 문제와 쓰레드 로컬 (Thread Local) (0) | 2024.05.07 |
| [Spring] 스프링 트랜잭션 전파 (0) | 2024.03.18 |
| [Spring] 스프링 트랜잭션 이해 (0) | 2024.03.15 |