스프링 핵심 원리(고급편)

Ch05. 동적 프록시 기술 - JDK 동적 프록시(적용)

webmaster 2022. 4. 11. 18:55
728x90

InvocationHandler

@Slf4j
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);
            //target 호출
            Object result = method.invoke(target, args);
            logTrace.end(status);
            return result;
        }catch (Exception e){
            logTrace.exception(status, e);
            throw e;
        }
    }
}
  • LogTraceBasicHandler는 InvocationHandler 인터페이스를 구현해서 JDK 동적 프록시에서 사용된다
  • private final Object target : 프록시가 호출할 대상이다.
  • String message = method.getDeclaringClass().getSimpleName() + "." ...
    • LogTrace에 사용할 메시지이다. 프록시를 직접 개발할 때는 "OrderController.request()" 와 같이 프록시마다 호출되는 클래스와 메서드 이름을 직접 남겼다.
    • 이제는 Method 를 통해서 호출되는 메서드 정보와 클래스 정보를 동적으로 확인할 수 있기 때문에 이 정보를 사용하면 된다.

빈 등록

@Configuration
public class DynamicProxyBasicConfig {

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

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

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1Impl 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 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;
    }
}
  • 이전에는 프록시 클래스를 직접 개발했지만, 이제는 JDK 동적 프록시 기술을 사용해서 각각의 Controller , Service , Repository에 맞는 동적 프록시를 생성해주면 된다.
  • LogTraceBasicHandler : 동적 프록시를 만들더라도 LogTrace를 출력하는 로직은 모두 같기 때문에 프록시는 모두 LogTraceBasicHandler를 사용한다

직접 프록시 사용 클레스 다이어그램
동적 프록시 생성 클레스 다이어그램
직접 프록시 사용 런타임 의존관계
동적 프록시 생성 런타임 의존관계

ProxyApplication 수정

@Import(DynamicProxyBasicConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {

   public static void main(String[] args) {
      SpringApplication.run(ProxyApplication.class, args);
   }
   @Bean
   public LogTrace logTrace(){
      return new ThreadLocalLogTrace();
   }
}
  • 동적 프록시를 빈으로 등록한 설정 파일을 Import 한다.
  • no-log에도 동적 프록시가 적용이 되어 로그가 남겨진다(no-log는 로그 남기면 X)

메서드 이름 필터 기능 추가

Pattern 에 Match 되는 것만 실행하도록 코드 수정

@Slf4j
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();
        //save.request, reque*, *est
        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);
            //target 호출
            Object result = method.invoke(target, args);
            logTrace.end(status);
            return result;
        }catch (Exception e){
            logTrace.exception(status, e);
            throw e;
        }
    }
}
  • logTraceFilterHandler는 기존 기능에 다음 기능이 추가되었다. 특정 메서드 이름이 매칭 되는 경우에만 LogTrace 로직을 실행한다. 이름이 매칭 되지 않으면 실제 로직을 바로 호출한다
  • 스프링이 제공하는 PatternMatchUtils.simpleMatch(..) 를 사용하면 단순한 매칭 로직을 쉽게 적용할 수 있다.
    • xxx : xxx가 정확히 매칭되면 참
    • xxx* : xxx로 시작하면 참
    • *xxx : xxx로 끝나면 참
    • *xxx* : xxx가 있으면 참
  • String [] patterns : 적용할 패턴은 생성자를 통해서 외부에서 받는다.

빈 등록시 패턴 추가

@Configuration
public class DynamicProxyFilterConfig {

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

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

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

    @Bean
    public OrderServiceV1 orderServiceV1(LogTrace logTrace) {
        OrderServiceV1Impl orderService = new OrderServiceV1Impl(orderRepositoryV1(logTrace));
        OrderServiceV1 proxy = (OrderServiceV1) Proxy
            .newProxyInstance(OrderServiceV1.class.getClassLoader(),
                new Class[]{OrderServiceV1.class},
                new LogTraceFilterHandler(orderService, logTrace, PATTERNS));
        return proxy;
    }

    @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 LogTraceFilterHandler(orderController, logTrace, PATTERNS));
        return proxy;
    }
}
  • public static final String [] PATTERNS = {"request*", "order*", "save*"};
    • 적용할 패턴이다. request , order , save로 시작하는 메서드에 로그가 남는다.
  • LogTraceFilterHandler : 앞서 만든 필터 기능이 있는 핸들러를 사용한다. 그리고 핸들러에 적용 패턴도 넣어준다.

ProxyApplication 수정

@Import(DynamicProxyFilterConfig.class)
@SpringBootApplication(scanBasePackages = "hello.proxy.app") //주의
public class ProxyApplication {

   public static void main(String[] args) {
      SpringApplication.run(ProxyApplication.class, args);
   }
   @Bean
   public LogTrace logTrace(){
      return new ThreadLocalLogTrace();
   }
}
  • 동적 프록시를 빈으로 등록한 설정 파일(Filter 기능 적용)을 Import 한다.
  • 실행해보면 no-log 가 사용하는 noLog() 메서드에는 로그가 남지 않는 것을 확인할 수 있다.
728x90