GoF 디자인 패턴에선 프록시를 사용하는 패턴인 프록시 패턴과 데코레이터 패턴을 의도에 따라 구분한다.
둘 다 프록시를 사용하지만, 의도가 다르다는 점이 핵심이다. 용어가 프록시 패턴이라 해서 이 패턴만 프록시를 사용하는 것은 아니다.
클라이언트와 서버 개념에서 일반적으로 클라이언트가 서버를 직접 호출하고, 처리 결과를 직접 받는다. 이를 직접 호출이라 한다.
클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자(Proxy)를 통해 대신 간접적으로 서버에 요청할 수 있다. 이를 간접 호출이라 한다.
프록시 객체가 중간에 있다면 크게 접근 제어와 부가 기능 추가를 수행할 수 있다.
위 클래스 의존관계를 보면 클라이언트는 ServerInterface
에만 의존한다. 그리고 서버와 프록시가 같은 인터페이스를 사용한다. 따라서 DI를 사용하여 대체 가능하다.
런타임(애플리케이션 실행 시점)에 클라이언트 객체에 DI를 사용해서 객체 의존관계를 변경해도 클라이언트 코드를 전혀 변경하지 않아도 된다. 클라이언트 입장에선 변경 사실조차 모른다. DI를 사용하면 클라이언트 코드의 변경 없이 유연하게 프록시를 주입할 수 있다.
client -> server
에서 client-> proxy
로 의존관계 변경핵심은 다음과 같다.
public interface Subject {
String operation();
}
@Slf4j
public class CacheProxy implements Subject {
private Subject target;
private String cacheValue;
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
log.info("프록시 호출");
if (cacheValue == null) {
cacheValue = target.operation();
}
return cacheValue;
}
}
@Slf4j
public class RealSubject implements Subject {
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000);
return "data";
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} cache (InterruptedException e) {
e.printStackTrace();
}
}
}
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
@Test
void cacheProxyTest() {
Subject realSubject = new RealSubject();
Subject cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
테스트 실행 결과 다음과 같다.
CacheProxy - 프록시 호출
RealSubject - 실제 객체 호출
CacheProxy - 프록시 호출
CacheProxy - 프록시 호출
캐시 프록시를 사용하면 최초 한번만 1초가 걸리고, 나머지는 거의 즉시 반환하게 된다.
원래 서버가 제공하는 기능에 더해 부가 기능을 수행한다.
Decorator
기능에 일부 중복이 있다. 꾸며주는 역할을 하는 Decorator
들은 스스로 존재할 수 없다. 항상 꾸며줄 대상이 있어야 한다. 따라서 내부의 호출 대상인 component
를 가지고 잇어야 한다. 또한 component
를 항상 호출해야 한다. 이 부분이 중복이다. 이런 중복을 제거하기 위해 component
를 속성으로 가지고 있는 Decorator
라는 추상 클래스를 만드는 방법도 고민할 수 있다. 이렇게 하면 추가로 클래스 다이어그램에서 어떤 것이 실제 컴포넌트인지, 데코레이터인지 명확하게 구분할 수 있다.
public interface Component {
String operation();
}
@Slf4j
public class RealComponent implements Component {
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
@Slf4j
public class MessageDecorator implements Component {
private Component component;
public MessageDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("MessageDecorator 실행");
String result = component.operation();
String decoResult = "*****" + result + "*****";
log.info("MessageDecorator 꾸미기 적용 전={}, 적용 후={}", result, decoResult);
return decoResult;
}
}
@Slf4j
public class TimeDecorator implements Component {
private Component component;
public TimeDecorator(Component component) {
this.component = component;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = component.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}ms", resultTime);
return result;
}
}
@Slf4j
public class DecoratorPatternClient {
private Component component;
public DecoratorPatternClient(Component component) {
this.component = component;
}
public void execute() {
String result = component.operation();
log.info("result={}", result);
}
}
@Test
void decorator2() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
Component timeDecorator = new TimeDecorator(messageDecorator);
DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
client.execute();
}
테스트 실행결과 client -> timeDecorator -> messageDecorator -> realComponent
의 객체 의존관계를 설정하고, 실행한다.
결과는 다음과 같다.
TimeDecorator 실행
MessageDecorator 실행
RealComponent 실행
MessageDecorator 꾸미기 적용 전=data, 적용 후=*****data*****
TimeDecorator 종료 resultTime=7ms
result=*****data*****