동적 프록시

JooHeon·2021년 12월 15일
0

🖊 동적 프록시란?

먼저 다음 코드가 있다.

log.info("Start");
String result1 = target.callA();
log.info("result={}", result1);

log.info("Start");
String result2 = target.callB();
log.info("result={}", result2);

두 문단의 코드는 중간에 뭘 호출하는 지를 제외하면 같다.
기존 프록시는 적용 대상의 수 만큼 프록시 객체를 만들어야 했지만
동적 프록시는 target.callA()와 target.callB()와 같은 공통 로직이 아닌 부분을
동적으로 처리하기 위한 기술이다.

🖊 리플렉션

리플렉션을 이용하면 실행하는 메소드를 동적으로 바꿀 수 있다.
아래의 코드가 있다.

// 클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");

Hello target = new Hello();

//callA 메서드 정보
Method methodCallA = classHello.getMethod("callA");
Object result1 = methodCallA.invoke(target);
log.info("result={}",result1);

//callB 메서드 정보
Method methodCallB = classHello.getMethod("callB");
Object result2 = methodCallB.invoke(target);
log.info("result={}",result2);

메소드를 직접 호출하던 부분이
classHello.getMethod에서 callA와 callB는 문자열로 동적으로 바꿀 수 있다.
이걸 공통로직과 분리하면 다음 코드와 같다.

// 클래스 정보
Class classHello = Class.forName("hello.proxy.jdkdynamic.ReflectionTest$Hello");

Hello target = new Hello();
        
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");
    Object result = method.invoke(target);
    log.info("end");
}

리플렉션 기술은 컴파일 시점에 오류를 잡을 수 없기 때문에 가급적 사용하면 안 된다.

🖊 JDK 동적 프록시

자바에서 지원하는 동적 프록시 기술이다. 다음 코드가 있다.

// 프록시 객체
@Slf4j
@RequiredArgsConstructor
public class TimeInvocationHandler implements InvocationHandler {

    private final Object 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 endTimeTime = System.currentTimeMillis();
        long resultTime = endTimeTime - startTime;
        log.info("TimeProxy 종료 resultTime = {}", resultTime);
        return result;
    }
}

// 테스트
@Test
public void dynamicA() throws Exception{
    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());
}

// 결과
22:50:06.834 [main] INFO TimeInvocationHandler - TimeProxy 실행
22:50:06.838 [main] INFO AImpl - call A
22:50:06.839 [main] INFO TimeInvocationHandler - TimeProxy 종료 resultTime = 0
22:50:06.840 [main] INFO dkDynamicProxyTest - targetClass=AImpl
22:50:06.840 [main] INFO dkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy9

InvocationHandler를 구현한 TimeInovcationHandler의 invoke 메소드에 공통 로직을 넣고
Proxy.newProxyInstance에 타겟의 인터페이스 클래스 로더, 인터페이스들, 구현한 핸들러를 넣는다.
생성된 proxy 객체로 타겟의 메소드를 호출하면 모두 구현한 invoke 메소드를 호출하게 된다.
    

🖊 인터페이스가 없는 객체는?

JDK 동적 프록시로 동적 프록시를 만들 대상은 인터페이스가 있어야한다.
그렇다면 인터페이스가 없는 객체를 동적 프록시를 만들기 위해서는 어떻게 해야할까?
바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는
라이브러리인 Code Generator Library(CGLIB)가 있다.

🖊 CGLIB

CGLIB를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다.
본래 외부 라이브러리지만 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다.
CGLIB를 직접 사용하는 경우는 거의 없고 프록시 팩토리를 통해 편리하게 사용할 수 있다.

// 공통 로직
@Slf4j
@RequiredArgsConstructor
public class TimeMethodInterceptor implements MethodInterceptor {

    private final Object 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 endTimeTime = System.currentTimeMillis();
        long resultTime = endTimeTime - startTime;
        log.info("TimeProxy 종료 resultTime = {}", resultTime);
        return result;
    }
}


// 테스트
@Test
public void cglib() throws Exception{
        ConcreteService target = new ConcreteService();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ConcreteService.class);
        enhancer.setCallback(new TimeMethodInterceptor(target));
        ConcreteService proxy = (ConcreteService) enhancer.create();

        proxy.call();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());
    }

cglib의 enhancer를 생성하고 setSuperclass에 프록시를 생성할 구현체를 넣는다.
MethodInterceptor는 Callback 인터페이스를 상속받고있다.
setCallback에 구현한 MethodInterceptor를 넣는다.

CGLIB는 클래스 기반이므로 상속으로 오는 제약사항을 받는다. 그런데 스프링 4.0 스프링 부트 2.0부터는 objenesis라는 라이브러리로 객체를 생성자 없이 생성할 수 있게 되어 상속으로 오는 제약사항 일부를 해결해 스프링은 기본으로 cglib를 사용한다.

🤔 주관적인 동적 프록시 이해

  1. 리플렉션과 같은 기술로 프록시 객체를 만들고 Object 객체기 때문에 실제 객체로 캐스팅 해준다.
  2. 프록시 객체에서 호출되는 메서드는 내부적으로 파싱? 되고 InvocationHanlder나 MethodInterception의 콜백함수를 호출한다.
  3. 콜백 함수에서 공통 로직과 파싱?된 Method 객체에 invoke로 타겟정보를 넣어 핵심 로직을 처리할 수 있다.

0개의 댓글