프록시 패턴과 데코레이터 패턴 정리

여기서 생각해보면 Decorator 기능에 일부 중복이 있다.
꾸며주는 역할을 하는 Decorator 들은 항상 꾸며줄 대상이 있어야 한다. 따라서 내부에 호출 대상인 component 를 가지고 있다. 그리고 component 를 항상 호출해야 한다.
public class XXXDecorator implements Component{
private final Component component;
public XXXDecorator(Component component){
this.component = component;
}
@Override
public String operation() {
...
String result = component.operation();
...
}
}
이런 중복을 제거하기 위해 component 를 속성으로 가지고 있는 Decorator 라는 추상 클래스를 만드는 방법도 고민할 수 있다. 이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트 인지, 데코레이터인지 명확하게 구분할 수 있다. 여기까지 고민한 것이 바로 GOF에서 설명하는 데코레이터 패턴의 기본 예제이다.
프록시 패턴 vs 데코레이터 패턴
Decorator 라는 추상 클래스를 만들어야 데코레이터 패턴일까?
프록시 패턴과 데코레이터 패턴은 그 모양이 거의 비슷한 것 같은데?
의도(intent)
사실 프록시 패턴과 데코레이터 패턴은 그 모양이 거의 같고, 상황에 따라 정말 똑같을 때도 있다.
그러면 둘을 어떻게 구분하는 것일까?
디자인 패턴에서 중요한 것은 해당 패턴의 겉모양이 아니라 그 패턴을 만든 의도가 더 중요하다. 따라서 의도에 따라 패턴을 구분한다.
- 프록시 패턴의 의도: 다른 개체에 대한 접근을 제어하기 위해 대리자를 제공
- 데코레이터 패턴의 의도: 객체에 추가 책임(기능)을 동적으로 추가하고, 기능 확장을 위한 유연한 대안 제공
정리
프록시를 사용하고 해당 프록시가 접근 제어가 목적이라면 프록시 패턴이고, 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴이 된다.
인터페이스 기반 프록시 - 적용
인터페이스와 구현체가 있는 V1 App에 지금까지 학습한 프록시를 도입해서 LogTrace 를 사용해보자.
프록시를 사용하면 기존 코드를 전혀 수정하지 않고 로그 추적 기능을 도입할 수 있다.
V1 기본 클래스 의존 관계

클라이언트(구현체)들은 인터페이스에 의존하기 때문에 프록시로 대체가 가능하다.
V1 런타임 객체 의존 관계

여기에 로그 추적용 프록시를 추가하면 다음과 같다.
V1 프록시 의존 관계 추가

Controller , Service , Repository 각각 인터페이스에 맞는 프록시 구현체를 추가한다.
(그림에서 리포지토리는 생략)
V1 프록시 런타임 객체 의존 관계

프록시 구현체가 로그를 남기고 실제 구현체를 호출한다.
애플리케이션 실행 시점에 프록시를 사용하도록 의존 관계를 설정해주어야 한다. 이 부분은 빈을 등록하는 설정 파일을 활용하면 된다.
(그림에서 리포지토리는 생략)
V1 로그 추적용 프록시 컨트롤러 ( OrderControllerInterfaceProxy )
@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() {
//보안상 이유로 로그X 단순히 위임만 담당
return target.noLog();
}
}
OrderControllerV1 target : 프록시가 실제 호출할 원본 컨트롤러의 참조를 가지고 있어야 한다.
V1 프록시 런타임 객체 의존 관계 설정 ( InterfaceProxyConfig )
@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);
}
}
- 프록시를 스프링 빈으로 등록하고, 실제 객체는 스프링 빈으로 등록하지 않는다.
- 프록시는 내부에 실제 객체를 참조하고 있다. 프록시 객체의 생성자에 실제 객체를 넣어준다.
- 실제 객체는 의존할 객체의 프록시(스프링 빈)를 참조하고 있다.
- LogTrace 객체가 스프링 빈으로 등록되어 있음을 전제로 한다.
스프링 빈으로 실제 객체 대신에 프록시 객체를 등록했기 때문에 앞으로 스프링 빈을 주입 받으면 실제 객체 대신에 프록시 객체가 주입된다.
실제 객체가 스프링 빈으로 등록되지 않는다고 해서 사라지는 것은 아니다. 프록시 객체가 실제 객체를 참조하기 때문에 프록시를 통해서 실제 객체를 호출할 수 있다. 쉽게 이야기해서 프록시 객체 안에 실제 객체가 있는 것이다.
스프링 빈으로 등록되는 객체

실제 객체가 스프링 빈으로 등록된다.

스프링 컨테이너에 프록시 객체가 등록된다.
실제 객체는 스프링 컨테이너와는 상관이 없다. 실제 객체는 프록시 객체를 통해서 참조될 뿐이다.
프록시 객체는 스프링 컨테이너가 관리하고 자바 힙 메모리에도 올라간다. 반면에 실제 객체는 자바 힙 메모리에는 올라가지만 스프링 컨테이너가 관리하지는 않는다.
정리
추가된 요구사항
1. 원본 코드를 전혀 수정하지 않고, 로그 추적기를 적용해라.
2. 특정 메서드는 로그를 출력하지 않는 기능
보안상 일부는 로그를 출력하면 안된다.
3. 다음과 같은 다양한 케이스에 적용할 수 있어야 한다.
v1 - 인터페이스가 있는 구현 클래스에 적용
v2 - 인터페이스가 없는 구체 클래스에 적용
v3 - 컴포넌트 스캔 대상에 기능 적용
프록시와 DI 덕분에 원본 코드를 전혀 수정하지 않고, 로그 추적기를 도입할 수 있었다. 물론 너무 많은 프록시 클래스를 만들어야 하는 단점이 있다.