<Spring> 프록시 패턴 적용

라모스·2022년 6월 21일
0

Spring☘️

목록 보기
15/18
post-thumbnail

이전 포스팅에 정리했던 프록시 패턴을 실제 로그 추적기에 적용해보려 한다.

인터페이스 기반 프록시와 클래스 기반 프록시가 있는데 방법은 비슷하기 때문에, 인터페이스 기반 프록시만 정리하고자 한다.

인터페이스 기반 프록시 적용

기본 클래스 의존 관계는 다음과 같았다.

여기에 프록시를 추가하면 다음과 같다.

적용 예시

@RequiredArgsConstructor
public class OrderRepositoryInterfaceProxy implements OrderRepositoryV1 {

    private final OrderRepositoryV1 target;
    private final LogTrace logTrace;

    @Override
    public void save(String itemId) {

        TraceStatus status = null;
        try {
            status = logTrace.begin("OrderRepository.request()");
            // target 호출
            target.save(itemId);
            logTrace.end(status);
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

@RequiredArgsConstructor
public class OrderServiceInterfaceProxy implements OrderServiceV1 {

    private final OrderServiceV1 target;
    private final LogTrace logTrace;

    @Override
    public void orderItem(String itemId) {

        TraceStatus status = null;
        try {
            status = logTrace.begin("OrderService.orderItem()");
            // target 호출
            target.orderItem(itemId);
            logTrace.end(status);
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }
}

@RequiredArgsConstructor
public class OrderControllerInterfaceProxy implements OrderControllerV1 {

    private final OrderControllerV1 target;
    private final LogTrace logTrace;

    @Override
    public String request(String itemId) {

        TraceStatus status = null;
        try {
            status = logTrace.begin("OrderController.request()");
            // target 호출
            String result = target.request(itemId);
            logTrace.end(status);
            return result;
        } catch (Exception e) {
            logTrace.exception(status, e);
            throw e;
        }
    }

    @Override
    public String noLog() {
        return target.noLog();
    }
}
@Configuration
public class InterfaceProxyConfig {

    @Bean
    public OrderControllerV1 orderController(LogTrace logTrace) {
        OrderControllerV1Impl controllerImpl
            = new OrderControllerV1Impl(orderService(logTrace));
        return new OrderControllerInterfaceProxy(controllerImpl, logTrace);
    }

    @Bean
    public OrderServiceV1 orderService(LogTrace logTrace) {
        OrderServiceV1Impl serviceImpl
            = new OrderServiceV1Impl(orderRepository(logTrace));
        return new OrderServiceInterfaceProxy(serviceImpl, logTrace);
    }

    @Bean
    public OrderRepositoryV1 orderRepository(LogTrace logTrace) {
        OrderRepositoryV1Impl repositoryImpl
            = new OrderRepositoryV1Impl();
        return new OrderRepositoryInterfaceProxy(repositoryImpl, logTrace);
    }
}

기존에는 스프링 빈이 orderControllerV1Impl, orderServiceV1Impl 같은 실제 객체를 반환했다. 하지만 프록시를 사용했기 때문에 프록시를 생성하고 프록시를 실제 스프링 빈 대신 등록한다. 실제 객체는 스프링 빈으로 등록하지 않는다.

프록시는 내부에 실제 객체를 참조하고 있다. OrderServiceInterfaceProxy는 내부에 실제 대상 객체인 OrderServiceV1Impl을 가지고 있다.

정리하면 다음과 같은 의존 관계를 가지고 있다.

  • proxy -> target
  • orderServiceInterfaceProxy -> orderServiceV1Impl

스프링 빈으로 실제 객체 대신 프록시 객체를 등록했기 때문에 앞으로 스프링 빈을 주입 받으면 실제 객체 대신 프록시 객체가 주입된다.
실제 객체가 스프링 빈으로 등록되지 않는다고 해서 사라지는 것은 아니다. 프록시 객체가 실제 객체를 참조하기 때문에 프록시를 통해 실제 객체를 호출할 수 있다.


프록시 적용 후 스프링 컨테이너에 위와 같이 프록시 객체가 등록된다. 이 객체를 스프링 빈으로 관리하게 된다.

  • 실제 객체는 스프링 컨테이너와 상관이 없다. 실제 객체는 프록시 객체를 통해 참조될 뿐이다.
  • 프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라간다. 반면 실제 객체는 자바 힙 매모리엔 올라가지만, 스프링 컨테이너가 관리하진 않는다.

프록시와 DI 덕분에 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입할 수 있다. 다만 너무 많은 프록시 클래스를 만들어야 하는 단점이 있다.

클래스 기반 프록시는 역시 Java의 다형성을 활용했기 때문에 인터페이스 방식과 크게 다를바는 없다. 인터페이스가 없어도 프록시가 가능하다는 점만 알고 가자.

정리

  • 클래스 기반 프록시는 해당 클래스에만 적용할 수 있다.
  • 인터페이스 기반 프록시는 인터페이스만 같으면 모든 곳에 적용할 수 있다.
  • 클래스 기반 프록시는 상속을 사용하기 때문에 몇 가지 제약이 있다.
    • 부모 클래스의 생성자를 호출해야 함
    • 클래스에 final 키워드가 붙으면 상속이 불가능함
    • 메서드에 final 키워드가 붙으면 해당 메서드를 오버라이딩 할 수 없음

인터페이스 기반의 프록시는 상속의 제약에서 자유롭다. 프로그래밍 관점에서도 인터페이스를 사용하는 것이 역할과 구현을 명확하게 나누기 때문에 더 좋다. 인터페이스 기반 프록시의 단점은 인터페이스가 필요하다는 점이다. 인터페이스가 없다면 해당 프록시를 만들 수 없다.

이론적으론 모든 객체에 인터페이스를 도입해서 역할과 구현을 나누는 것이 좋다. 역할과 구현을 나누어 구현체를 매우 편리하게 변경할 수 있기 때문이다. 하지만, 실제로는 구현을 거의 변경할 일이 없는 클래스도 많다.
상황에 따라 인터페이스나 구체 클래스를 적절하게 사용하는 것이 좋다.

지금까지의 방식은 프록시 클래스를 너무 많이 만들어야 한다는 단점이 있었다. 로직은 모두 똑같고 대상 클래스만 다를 뿐이다. 프록시 클래스를 하나만 만들어서 모든 곳에 적용하고 싶다면 동적 프록시 기술을 사용해야 한다.

References

profile
Step by step goes a long way.

0개의 댓글