<Spring> 동적 프록시

라모스·2022년 6월 22일
0

Spring☘️

목록 보기
16/18
post-thumbnail

프록시를 사용해서 기존 코드를 변경하지 않고, 로그 추적기라는 부가 기능을 적용할 수 있었다. 문제는 대상 클래스 수 만큼 로그 추적을 위한 프록시 클래스를 만들어야 한다는 점이다.

Java가 기본으로 제공하는 JDK 동적 프록시 기술이나 CGLIB 같은 프록시 생성 오픈소스 기술을 활용하면 프록시 객체를 동적으로 만들어낼 수 있다. 프록시 클래스를 지금처럼 계속 만들지 않아도 되며, 프록시를 적용할 코드를 하나만 만들어두고 동적 프록시 기술을 사용해서 프록시 객체를 찍어내면 된다.

JDK 동적 프록시를 이해하려면 Java의 리플렉션 기술을 이해해야 한다. 리플렉션을 활용하면 클래스나 메서드의 메타정보를 동적으로 획득하고, 코드도 동적으로 호출할 수 있다.
자세한 사항은 이전에 정리했던 페이지를 확인하자.

리플렉션을 사용하면 클래스와 메서드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 하지만 리플렉션 기술은 런타임에 동작하기 때문에, 컴파일 시점에 오류를 잡을 수 없다. 해당 코드를 직접 실행하는 시점에 발생하는 런타임 오류가 발생한다. 리플렉션은 일반적으로 사용하면 안되고 프레임워크 개발이나 또는 매우 일반적인 공통 처리가 필요할 때 부분적으로 주의해서 사용해야 한다.

JDK 동적 프록시

동적 프록시 기술을 사용하면 개발자가 직접 프록시 클래스를 만들지 않아도 된다. 이름 그대로 프록시 객체를 동적으로 런타임에 개발자 대신 만들어준다. 그리고 동적 프록시에 원하는 실행 로직을 지정할 수 있다.
JDK 동적 프록시는 인터페이스를 기반으로 프록시를 동적으로 만들어준다. 따라서 인터페이스가 필수이다.

JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.

package java.lang.reflect;

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
    // (프록시 자신, 호출한 메서드, 메서드를 호출할 때 전달한 인수)
}

적용 예시

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;
        }
    }
}
  • Object target: 동적 프록시가 호출할 대상
  • method.invoke(target, args): 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다. args는 메서드 호출시 넘겨줄 인수이다.
@Configuration
public class DynamicProxyBasicConfig {

    @Bean
    public OrderControllerV1 orderControllerV1(LogTrace logTrace) {
        OrderControllerV1 orderController = new OrderControllerV1Impl(orderServiceV1(logTrace));

        OrderControllerV1 proxy = (OrderControllerV1) Proxy.newProxyInstance(OrderControllerV1.class.getClassLoader(),
                new Class[]{OrderControllerV1.class},
                new LogTraceBasicHandler(orderController, logTrace));

        return proxy;
    }

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));

        OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
                new Class[]{OrderServiceV1.class},
                new LogTraceBasicHandler(orderService, logTrace));

        return proxy;
    }

    @Bean
    public OrderRepositoryV1 orderRepositoryV1(LogTrace logTrace) {
        OrderRepositoryV1 orderRepository = new OrderRepositoryV1Impl();

        OrderRepositoryV1 proxy = (OrderRepositoryV1) Proxy.newProxyInstance(OrderRepositoryV1.class.getClassLoader(),
                new Class[]{OrderRepositoryV1.class},
                new LogTraceBasicHandler(orderRepository, logTrace));

        return proxy;
    }
}

실행 결과로 나오는 proxyClass=class com.sun.proxy.$Proxy1 이 부분이 동적으로 생성된 프록시 클래스 정보이다.

JDK 동적 프록시는 인터페이스가 필수이다. 인터페이스 없이 클래스만 있는 경우에는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해야 한다.

CGLIB: Code Generator Library

CGLIB는 바이트코드를 조작해서 동적으로 클래스를 생성하는 기술을 제공하는 라이브러리이다. 이를 사용하면 인터페이스가 없어도 구체 클래스만 가지고 동적 프록시를 만들어낼 수 있다. CGLIB는 외부 라이브러리인데, 스프링 프레임워크가 스프링 내부 소스 코드에 포함했다. 별도의 외부 라이브러리를 추가하지 않아도 사용할 수 있다.

CGLIB를 직접 사용하는 경우는 거의 없으므로 무엇인지 대략 개념만 잡고 넘어가자. 스프링의 ProxyFactory라는 것이 이 기술을 편리하게 사용할 수 있도록 도와준다.

JDK 동적 프록시에서 실행 로직을 위해 InvocationHandler를 제공했듯, CGLIB는 MethodInterceptor를 제공한다.

package org.springframework.cglib.proxy;

public interface MethodInterceptor extends callback {
    Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
    // (CGLIB가 적용된 객체, 호출된 메서드, 메서드를 호출하면서 전달된 인수, 메서드 호출에 사용)
}

예제

public interface ServiceInterface {
    void save();

    void find();
}

@Slf4j
public class ServiceImpl implements ServiceInterface {
    @Override
    public void save() {
        log.info("save 호출");
    }

    @Override
    public void find() {
        log.info("find 호출");
    }
}

@Slf4j
public class ConcreteService {
    public void call() {
        log.info("ConcreteService 호출");
    }
}
@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 동적 프록시의 예제와 거의 비슷하다. 이를 실행할 테스트 코드는 다음과 같다.

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

CGLIB는 Enhancer를 사용해서 프록시를 생성한다. 구체 클래스를 상속받아 프록시를 생성할 수 있는데, setSuperclass에서 어떤 구체 클래스를 상속받을지 지정한다. 이후 프록시에 적용할 실행 로직을 할당한 뒤, 프록시를 생성한다.

JDK 동적 프록시는 인터페이스를 구현해서 프록시를 만드는 반면, CGLIB는 구체 클래스를 상속해서 프록시를 만든다.

18:09:48.766 [main] INFO hello.proxy.cglib.CglibTest - targetClass=class hello.proxy.common.service.ConcreteService
18:09:48.768 [main] INFO hello.proxy.cglib.CglibTest - proxyClass=class hello.proxy.common.service.ConcreteService$$EnhancerByCGLIB$$25d6b0e3
18:09:48.768 [main] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 실행
18:09:48.775 [main] INFO hello.proxy.common.service.ConcreteService - ConcreteService 호출
18:09:48.775 [main] INFO hello.proxy.cglib.code.TimeMethodInterceptor - TimeProxy 종료 resultTime=7

결과에서 확인할 수 있는 $$EnhancerByCGLIB$$25d6b0e3는 CGLIB를 통해 생성된 프록시 클래스 이름이다.

클래스 기반 프록시는 상속을 사용하기 때문에 다음과 같은 제약이 있다.

  • 부모 클래스의 생성자를 체크해야 한다. → CGLIB는 자식 클래스를 동적으로 생성하기 때문에 기본 생성자가 필요하다.
  • 클래스에 final 키워드가 붙으면 상속이 불가능하다. → CGLIB에서는 예외가 발생한다.
  • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없다. → CGLIB에서는 프록시 로직이 동작하지 않는다.

정리

인터페이스가 있는 경우 JDK 동적 프록시를 적용하고, 그렇지 않은 경우에는 CGLIB를 적용해야 한다. 두 기술을 함께 사용하기 위해 JDK 동적 프록시의 InvocationHandler와 CGLIB의 MethodInterceptor를 각각 중복으로 만들어서 관리해야 할까? 스프링이 제공하는 ProxyFactory는 이 문제를 해결해준다.

References

profile
Step by step goes a long way.

0개의 댓글