모르지 않다는 것은 아는것과 다르다.

Spring

JDK 동적 프록시

채마스 2022. 8. 6. 23:39

개요

  • 프록시를 사용해서 기존의 코드를 건드리지 않고, 새로운 기능을 추가할 수 있다.
  • 하지만 프록시를 적용하고 싶은 대상의 숫자 만큼 프록시 클래스를 만들어야 한다는 단점이 있다.
  • 이러한 문제를 해결하는 방법이 바로 동적 프록시이다.
  • 그리고 동적프록시를 만드는 방법중 하나인 JDK 동적 프록시를 구현하는 방법을 알아보려고 한다.

 

코드 예시

  • 먼저 아래와 같이 2개의 인터페이스와 구현체를 구현한다.
  • 프록시를 적용하고 싶은 대상이 된다.
public interface AInterface {
    String call();
}
@Slf4j
public class AImpl implements AInterface {

    @Override
    public String call() {
        log.info("A 호출");
        return "a"; 
    }
}
public interface BInterface {
      String call();
}
@Slf4j
public class BImpl implements BInterface {

    @Override
    public String call() {
        log.info("B 호출");
        return "b"; 
    }
}
  • JDK 동적 프록시에 적용할 로직은 InvocationHandler 인터페이스를 구현해서 작성하면 된다.
@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;

    }
}
  • invoke 메소드의 인자는 아래와 같다.
    • proxy: 프록시 자신
    • method: 호출한 메서드
    • args: 메소드를 호출할 때 전달한 인수
  • 먼저 동적 프록시가 호출할 대상인 target을 인자로 가지고 있다.
  • 그리고 method.invoke(target, args); -> 리플렉션을 사용해서 target 인스턴스의 메서드를 실행한다.
@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();
    }

    @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();
    }
}
  • new TimeInvocationHandler(target);: 동적 프록시에 적용할 핸들러 로직이다.
  • newProxyInstance: 이 메소드를 사용해서 동적 프록시를 생성한다.
    • 인자로 클래스 로더 정보, 인터페이스, 그리고 핸들러 로직을 넣어주면 된다.
    • 그렇게 되면 해당 인터페이스를 기반으로 동적 프록시를 생성하고 그 결과를 반환한다.

 

애플리케이션에 적용

  • 아래와 같이 애플리케이션이 구현되어 있다고 가정하자

  • 여기서 OrderController과 OrderService에 동적 프록시를 적용한다면 아래와 같은 구조가 될 것이다.

  • 런타임 시에는 아래와 같이 의존 관계가 형성된다.

 

코드 구현

  • 위와 마찬가지로 동적 프록시에 적용할 로직을 구현하기 위해서 InvocationHandler를 구현해서 LogTraceBasicHandler를 아래와 같이 구현한다.
public class LogTraceFilterHandler implements InvocationHandler {

    private final Object target;
    private final LogTrace logTrace;
    private final String[] patterns;

    public LogTraceFilterHandler(Object target, LogTrace logTrace, String[] patterns) {
        this.target = target;
        this.logTrace = logTrace;
        this.patterns = patterns;
    }

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

        //메서드 이름 필터
        String methodName = method.getName();
        if (!PatternMatchUtils.simpleMatch(patterns, methodName)) {
            return method.invoke(target, args);
        }

        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;
        }
    }
}
  • target: 프록시가 호출할 대상이다.
  • PatternMatchUtils.simpleMatch를 이용해서 특정 메서드 이름이 매칭 되는 경우에만 LogTrace 로직을 실행하도록 구현한다.
    • xxx : xxx가 정확히 매칭되면 참 xxx* : xxx로 시작하면 참
    • *xxx : xxx로 끝나면 참
    • xxx : xxx가 있으면 참
  • 이름이 매칭되지 않으면 실제 로직을 바로 호출한다.

 

Config 설정

@Configuration
public class DynamicProxyFilterConfig {

    private static final String[] PATTERNS = {"request*", "order*", "save*"};

    @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 LogTraceFilterHandler(orderControllerV1, logTrace, PATTERNS));
        return proxy;
    }


    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1 orderServiceV1 = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        OrderServiceV1 proxy = (OrderServiceV1) Proxy.newProxyInstance(OrderServiceV1.class.getClassLoader(),
                new Class[]{OrderServiceV1.class},
                new LogTraceFilterHandler(orderServiceV1, logTrace, PATTERNS));
        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 LogTraceFilterHandler(orderRepository, logTrace, PATTERNS));
        return proxy;
    }
}
  • public static final String[] PATTERNS = {"request*", "order*", "save*"}; : request , order , save 로 시작하는 메서드에 로그가 남기기 위해서 패턴을 정의한다.
  • Controller , Service , Repository 에 맞는 동적 프록시를 생성해주면 된다.
  • 동적 프록시를 만들더라도 LogTrace 를 출력하는 로직은 모두 같기 때문에 프록시는 모두 LogTraceBasicHandler 를 사용한다.
  • Controller , Service , Repository 모두 proxy를 반환한다. -> 의존관계를 proxy로 설정한다.
  • 대신에 proxy에서 실제 구현체를 target으로 가지고 있고, proxy 로직이 수행되는 중간에 실제 구현체를 실행한다.

 

 

References

  • 김영한님의 스프링 고급편

'Spring' 카테고리의 다른 글

SpringMVC를 이용해서 요청 Body값 Trim처리하기  (0) 2023.03.06
AbstractRoutingDataSource를 통한 Multi-DataSource 구현  (0) 2023.02.04
ApplicationEventPublisher  (0) 2022.06.11
MapStruct  (0) 2022.04.09
Handler Methods  (0) 2022.04.09