오봉이와 함께하는 개발 블로그

스프링 핵심 원리 - 고급편 > 프록시 팩토리 적용 1, 2, 정리 본문

BE/Spring

스프링 핵심 원리 - 고급편 > 프록시 팩토리 적용 1, 2, 정리

오봉봉이 2024. 10. 3. 23:53
728x90

스프링 핵심 원리 - 고급편 > 프록시 팩토리 적용 1

지금까지 학습한 프록시 팩토리를 사용해서 애플리케이션에 프록시를 만들어보자.
먼저 인터페이스가 있는 v1 애플리케이션에 LogTrace기능을 프록시 팩토리를 통해서 프록시를 만들어 적용해보자.

public class LogTraceAdvice implements MethodInterceptor {

  private final LogTrace logTrace;

  public LogTraceAdvice(LogTrace logTrace) {
    this.logTrace = logTrace;
  }

  @Override
  public Object invoke(MethodInvocation invocation) throws Throwable {
    TraceStatus status = null;
    try {
      Method method = invocation.getMethod();
      String message = method.getDeclaringClass().getSimpleName() + "." + method.getName() + "()";
      status = logTrace.begin(message);
      Object result = invocation.proceed();
      logTrace.end(status);
      return result;
    } catch (Exception e) {
      logTrace.exception(status, e);
      throw e;
    }
  }
}
@Slf4j
@Configuration
public class ProxyFactoryConfigV1 {

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

    ProxyFactory factory = new ProxyFactory(orderController);
    factory.addAdvisor(getAdvisor(logTrace));
    OrderControllerV1 proxy = (OrderControllerV1) factory.getProxy();
    log.info("ProxyFactory proxy = {}, target = {}", proxy.getClass(), orderController.getClass());
    return proxy;
  }

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

    ProxyFactory factory = new ProxyFactory(orderService);
    factory.addAdvisor(getAdvisor(logTrace));
    OrderServiceV1 proxy = (OrderServiceV1) factory.getProxy();
    log.info("ProxyFactory proxy = {}, target = {}", proxy.getClass(), orderService.getClass());
    return proxy;
  }


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

    ProxyFactory factory = new ProxyFactory(orderRepository);
    factory.addAdvisor(getAdvisor(logTrace));
    OrderRepositoryV1 proxy = (OrderRepositoryV1) factory.getProxy();
    log.info("ProxyFactory proxy = {}, target = {}", proxy.getClass(), orderRepository.getClass());
    return proxy;
  }

  private Advisor getAdvisor(LogTrace logTrace) {
    // pointcut
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedNames("request*", "order*", "save*");
    // advice
    LogTraceAdvice advice = new LogTraceAdvice(logTrace);
    // advisor = pointcut + advice
    return new DefaultPointcutAdvisor(pointcut, advice);
  }

}
  • 포인트컷은 NameMatchMethodPointcut을 사용한다.
    • 심플 매칭 기능이 있어 *를 사용할 수 있다.
    • request*, order*, save*
      • request로 시작하는 메서드에 포인트컷은 true 를 반환한다. 나머지도 같다.
      • 이렇게 설정한 이유는 noLog() 메서드에는 어드바이스를 적용하지 않기 위해서다.
  • 어드바이저는 포인트컷(NameMatchMethodPointcut), 어드바이스(LogTraceAdvice)를 가지고 있다.
  • 프록시 팩토리에 각각의 targetadvisor를 등록해서 프록시를 생성한다. 그리고 생성된 프록시를 스프링 빈으로 등록한다.
@Import(ProxyFactoryConfigV1.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();
    }

}

애플리케이션 로딩 로그

ProxyFactory proxy = class com.sun.proxy.$Proxy50, target = class hello.proxy.app.v1.OrderRepositoryV1Impl
ProxyFactory proxy = class com.sun.proxy.$Proxy52, target = class hello.proxy.app.v1.OrderServiceV1Impl
ProxyFactory proxy = class com.sun.proxy.$Proxy53, target = class hello.proxy.app.v1.OrderControllerV1Impl

V1 애플리케이션은 인터페이스가 있기 때문엔 프록시 팩토리가 JDK 동적 프록시를 적용한다.
애플리케이션 로딩 로그를 통해 확인할 수 있다.

실행 로그

[b83177a1] OrderControllerV1.request()
[b83177a1] |-->OrderServiceV1.orderItem()
[b83177a1] |   |-->OrderRepositoryV1.save()
[b83177a1] |   |<--OrderRepositoryV1.save() time=1003ms
[b83177a1] |<--OrderServiceV1.orderItem() time=1004ms
[b83177a1] OrderControllerV1.request() time=1004ms

스프링 핵심 원리 - 고급편 > 프록시 팩토리 적용 2

이번에는 인터페이스가 없고 구체 클래스만 있는 V2에 적용해보자.

@Slf4j
@Configuration
public class ProxyFactoryConfigV2 {
  @Bean
  public OrderControllerV2 orderControllerV2(LogTrace logTrace) {
    OrderControllerV2 orderController = new OrderControllerV2(orderServiceV2(logTrace));

    ProxyFactory factory = new ProxyFactory(orderController);
    factory.addAdvisor(getAdvisor(logTrace));
    OrderControllerV2 proxy = (OrderControllerV2) factory.getProxy();
    log.info("ProxyFactory proxy = {}, target = {}", proxy.getClass(), orderController.getClass());
    return proxy;
  }

  @Bean
  public OrderServiceV2 orderServiceV2(LogTrace logTrace) {
    OrderServiceV2 orderService = new OrderServiceV2(orderRepositoryV2(logTrace));

    ProxyFactory factory = new ProxyFactory(orderService);
    factory.addAdvisor(getAdvisor(logTrace));
    OrderServiceV2 proxy = (OrderServiceV2) factory.getProxy();
    log.info("ProxyFactory proxy = {}, target = {}", proxy.getClass(), orderService.getClass());
    return proxy;
  }


  @Bean
  public OrderRepositoryV2 orderRepositoryV2(LogTrace logTrace) {
    OrderRepositoryV2 orderRepository = new OrderRepositoryV2();

    ProxyFactory factory = new ProxyFactory(orderRepository);
    factory.addAdvisor(getAdvisor(logTrace));
    OrderRepositoryV2 proxy = (OrderRepositoryV2) factory.getProxy();
    log.info("ProxyFactory proxy = {}, target = {}", proxy.getClass(), orderRepository.getClass());
    return proxy;
  }

  private Advisor getAdvisor(LogTrace logTrace) {
    // pointcut
    NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
    pointcut.setMappedNames("request*", "order*", "save*");
    // advice
    LogTraceAdvice advice = new LogTraceAdvice(logTrace);
    // advisor = pointcut + advice
    return new DefaultPointcutAdvisor(pointcut, advice);
  }
}
@Import(ProxyFactoryConfigV2.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();
    }

}

애플리케이션 로딩 로그

ProxyFactory proxy = class hello.proxy.app.v2.OrderRepositoryV2$$EnhancerBySpringCGLIB$$ec5365c6, target = class hello.proxy.app.v2.OrderRepositoryV2
ProxyFactory proxy = class hello.proxy.app.v2.OrderServiceV2$$EnhancerBySpringCGLIB$$40a393e9, target = class hello.proxy.app.v2.OrderServiceV2
ProxyFactory proxy = class hello.proxy.app.v2.OrderControllerV2$$EnhancerBySpringCGLIB$$ee5a8c7c, target = class hello.proxy.app.v2.OrderControllerV2

V2 애플리케이션은 인터페이스가 없고 구체 클래스만 있기 때문에 프록시 팩토리가 CGLIB을 적용한다.
애플리케이션 로딩 로그를 통해서 CGLIB 프록시가 적용된 것을 확인할 수 있다.

실행 로그

[9597071b] OrderControllerV2.request()
[9597071b] |-->OrderServiceV2.orderItem()
[9597071b] |   |-->OrderRepositoryV2.save()
[9597071b] |   |<--OrderRepositoryV2.save() time=1005ms
[9597071b] |<--OrderServiceV2.orderItem() time=1007ms
[9597071b] OrderControllerV2.request() time=1009ms

정리

프록시 팩토리 덕분에 개발자는 매우 편리하게 프록시를 생성할 수 있게 되었다.
추가로 어드바이저, 어드바이스, 포인트컷 이라는 개념 덕분에 어떤 부가 기능어디에 적용할 지 명확하게 이해할 수 있었다.

남은 문제

프록시 팩토리와 어드바이저 같은 개념을 사용하여 지금까지 고민했던 문제들(원본 코드 변경 없이 적용, 어디에 어떻게 적용 등)은 해결되었다. 프록시도 깔끔하게 적용하고 포인트컷으로 어디에 부가 기능을 적용할지 명확하게 정의가 가능해졌다.
원본 코드를 전혀 손대지 않고 프록시를 통해 부가 기능도 적용할 수 있었지만 아직 해결되지 않는 문제들이 있다.

문제1 - 너무 많은 설정

바로 ProxyFactoryConfigV1, ProxyFactoryConfigV2와 같은 설정 파일이 지나치게 많다는 점이다.
예를 들어서 애플리케이션에 스프링 빈이 100개가 있다면 여기에 프록시를 통해 부가 기능을 적용하려면 100개의 동적 프록시 생성 코드를 만들어야 한다...
무수히 많은 설정 파일 때문에 설정 지옥을 경험하게 될 것이다.
최근에는 스프링 빈을 등록하기 귀찮아서 컴포넌트 스캔까지 사용하는데, 이렇게 직접 등록하는 것도 모자라서, 프록시를 적용하는 코드까지 빈 생성 코드에 넣어야 한다.

문제2 - 컴포넌트 스캔

애플리케이션 V3처럼 컴포넌트 스캔을 사용하는 경우 지금까지 학습한 방법으로는 프록시 적용이 불가능하다.
왜냐하면 실제 객체를 컴포넌트 스캔으로 스프링 컨테이너에 스프링 빈으로 등록을 다 해버린 상태이기 때문이다. 지금까지 학습한 프록시를 적용하려면, 실제 객체를 스프링 컨테이너에 빈으로 등록하는 것이 아니라 ProxyFactoryConfigV1에서 한 것 처럼, 부가 기능이 있는 프록시를 실제 객체 대신 스프링 컨테이너에 빈으로 등록해야 한다.

두 가지 문제를 한번에 해결하는 방법이 바로 다음에 설명할 빈 후처리기이다.

출처: 김영한 지식공유자의 스프링 핵심 원리 고급편

728x90
Comments