WEB/Spring

스프링 핵심 원리 - 고급편 4) 동적 프록시 기술

Tony Lim 2022. 6. 7. 12:40
728x90

리플렉션

void reflection0() {
    Hello target = new Hello();

    //공통 로직1 시작
    log.info("start");
    String result1 = target.callA(); //호출하는 메서드가 다음
    log.info("result={}", result1);
    //공통 로직1 종료

    //공통 로직2 시작
    log.info("start");
    String result2 = target.callB(); //호출하는 메서드가 다음
    log.info("result={}", result2);
    //공통 로직2 종료
}

거의 로직1 과 로직 2가 유사하지만 공통 메소드로 묶기가 힘들다. 중간에 호출하는 메서드가 다르기 때문이다.

    void reflection2() throws Exception {
        //클래스 정보
        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("result={}", result);
    }

리플렉션을 통해 class의 metadata를 얻어오고 기존의 callA(), callB() 가 Method로 대체가 되었다.

공통로직으로 뽑아 낼수 있게 된것이다.

하지만 런타임에 동작하기에 컴파일타임에 오류를 잡을수 없다.

 


 

JDK 동적 프록시

인터페이스가 있어야만 프록시를 만들 수 있다.

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;
    }
}

어떤 method가 호출될지 넘어오게 된다. 그것을 그냥 invoke로 호출하고 그 결과를 어떻게 추가적으로 처리할 것인지를 작성하면 된다.

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());
}

handler가 위에서 정의한 일종의 데코레이터 느낌으로 newProxyInstance인자로 넣어준다. 그러면 해당 feature가 적용된 상태의 결과 값을 얻을 수 있게 된다.

proxy.call 을 하게되면 위에 handler에게 call method가 전달되게 된다. handler는 실제 구현체인 AImpl을 target으로 가지고 있으니 해당 구현체의 call 을 호출하게 된다.

 

JDK 동적프록시를 사용하면 적용 대상만큼 프록시 객체를 내가 일일이 만들 필요가 없다.
또한 적용하려는 로직을 InvocationHandler로 만들고 프록시를 만들때 넣어주면 된다.

 


 

Controller에 적용해보자

public class LogTraceBasicHandler implements InvocationHandler {

    private final Object target;
    private final LogTrace logTrace;

    public LogTraceBasicHandler(Object target, LogTrace logTrace) {
        this.target = target;
        this.logTrace = logTrace;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        TraceStatus status = null;
        try {
            String message = method.getDeclaringClass().getSimpleName() + "." +
                    method.getName() + "()";
            status = logTrace.begin(message);

            //로직 호출
            Object result = method.invoke(target, args);
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}
@Configuration
public class DynamicProxyBasicConfig {

    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderControllerV1 = new OrderControllerV1Impl(orderServiceV1(logTrace));
        OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
                new Class[]{OrderControllerV1.class},
                new LogTraceBasicHandler(orderControllerV1, logTrace));
        return proxy;
    }

@Configuration 에서 spring으로 하여금 controller proxy bean을 관리하게 만든다.

어떤 클래스로더로 불러 올것인가?
어떤 인터페이스를 기반으로 프록시 클래스를 만들것인가?
만든 프록시 클래스에 어떤 로직을 더 추가적으로 적용하고 실제 로직은 어디서 호출하는가?

 


 

CGLIB 

부모 클래스의 생성자를 체크 해야함 -> CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본생성자가 필요하다.

클래스나 메서드에 final 이 붙으면 상속 및 오버라이딩 할 수 없다. -> CGLIB에서 예외발생하거나 , 프록시로직이 동작하지 않음

JDK 동적 프록시의 InvocationHandler 처럼 CGLIB 는 MethodInterceptor를 구현해서 로직을 추가하면 된다.

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;
    }
}

JDK동적프록시와 동일하게 method.invoke(target,args)를 해도 되지만 methodProxy를 통해 호출하는 것이 내부적으로 최적화 된 기술을 사용할 수 있다.

@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();

}

ConcreteService는 클래스이다. 인터페이스가 아니다.
해당 클래스를 상속받은 프록시를 만들기위해 setSuperClass 에 구체클래스를 지정해준다.

728x90