Proxy Pattern, Decorator Pattern은 모두 Proxy 기술(클라이언트의 요청을 대신해서 처리해 주는 역할)이 적용됩니다.
이때, 서버와 프록시는 같은 인터페이스를 사용해야 하고, 의존관계를 서버에서 프록시로 변경해도 클라이언트 입장에서는 이러한 사실을 몰라야 합니다(프록시 체인). 앞서 설명드린 DI를 사용하면, 클라이언트의 코드 변경 없이 프록시를 주입할 수 있게 됩니다.
이제 이 둘의 차이와 역할에 대해 설명드리겠습니다.
GOF 디자인 패턴에 따라 프록시의 역할은 2가지로 구분됩니다(intent에 의해 구분).
1. Proxy Pattern
- 권한에 따른 접근 차단, 캐싱, 지연로딩
2. Decorator Pattern
- 원래 서버가 제공하는 기능에 더해 부가 기능 수행
Proxy Pattern 적용 전 예시
@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);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
실제 객체를 호출하고, DB나 외부에서 데이터를 조회하기 위해 1초를 쉬도록 설계하였습니다. 한 번 객체를 호출하고, 불변 객체라면 임의의 저장소에 보관해 두고 꺼내서 사용하는 것이 성능적으로 좋습니다. 이러한 기능을 제공하는 것을 캐시라고 부릅니다.
프록시 패턴의 주요한 기능은 캐싱 기능입니다.
@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; // 청므 조회 이후에는 캐시(cacheValue)에서 매우 빠르게 데이터 조회
}
}
프록시도 실제 객체와 모양이 같아야 하기 때문에, Subject를 DI 시켰고 operation() 메서드에는 처음 객체를 호출한 경우 실제 객체를 호출하고 그렇지 않다면, 프록시 객체를 호출하도록 반환하도록 구현하였습니다.
<테스트 코드>
class ProxyPatternClientTest {
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
@Test
void cacheProxyTest() {
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
}
<결과>
결과적으로 캐시 프록시를 도입하기 전에는 실제 객체를 3번 호출했지만, 프록시 패턴을 적용시킨 후에는 캐싱 기술을 이용해 한 번만 호출했음을 확인할 수 있습니다. 프록시 패턴의 핵심은 Client의 코드를 변경하지 않고, 프록시를 도입하여 접근 제어를 하는 것입니다.
Decorator Pattern
- 기존 서버의 기능에 부가 기능 추가
@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);
}
}
Component 인터페이스에는 프록시가 호출해야 하는 대상(target) 정보가 담겨있으므로 의존관계 주입을 해주었습니다.
이제 데코레이터 패턴을 적용하여 반환값을 변형해 주는 데코레이터, 실행 시간을 측정해 주는 데코레이터를 만들어 보겠습니다.
MessageDecorator
@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;
}
}
TimeDecorator
@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;
}
}
<테스트 코드>
@Test
void decorator2() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
Component timeDecorator = new TimeDecorator(messageDecorator);
DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
client.execute();
}
기존 서버에 요청을 보내는 기능에 프록시를 이용하여 반환값을 변경해 주고, 실행 시간을 출력해 주는 기능 구현할 수 있게 되었습니다.
클래스 의존관계와 애플리케이션 실행 시점에서의 의존관계는 다음과 같습니다.
부가 기능을 수행한다는 점에서 데코레이터 패턴은 큰 이점을 지니고 있지만, 항상 Component의 정보를 가지고 있어야 한다는 단점이 있습니다. 그래서 이러한 중복 로직을 없애기 위해 추상 클래스를 만드는 방법도 고민할 수 있습니다. 추상 클래스를 사용하여 중복을 제거하는 로직을 코드로 보여드리겠습니다.
// Component 인터페이스
interface Component {
String operation();
}
// ConcreteComponent 클래스
class ConcreteComponent implements Component {
@Override
public String operation() {
return "ConcreteComponent";
}
}
// Decorator 추상 클래스
abstract class Decorator implements Component {
protected Component component;
public Decorator(Component component) {
this.component = component;
}
@Override
public abstract String operation();
}
// ConcreteDecorator 클래스
class ConcreteDecoratorA extends Decorator {
public ConcreteDecoratorA(Component component) {
super(component);
}
@Override
public String operation() {
return "ConcreteDecoratorA(" + component.operation() + ")";
}
}
class ConcreteDecoratorB extends Decorator {
public ConcreteDecoratorB(Component component) {
super(component);
}
@Override
public String operation() {
return "ConcreteDecoratorB(" + component.operation() + ")";
}
}
// 클라이언트 코드
public class DecoratorExample {
public static void main(String[] args) {
Component component = new ConcreteComponent();
Component decoratorA = new ConcreteDecoratorA(component);
Component decoratorB = new ConcreteDecoratorB(decoratorA);
System.out.println(decoratorB.operation());
}
}
프록시를 이용해 원본 코드를 바꾸지 않고, 기능을 추가(Decorator Pattern)하거나 접근을 제어(Proxy Pattern)하는 디자인 패턴에 대해 알아보았습니다.
<참고자료>
'Java > Spring' 카테고리의 다른 글
[Spring] 동시성 이슈 및 해결 방안 (0) | 2023.09.12 |
---|---|
[Spring] Spring AOP (0) | 2023.08.15 |
[Spring] Template Method Pattern, Strategy Pattern, Template Callback Pattern (0) | 2023.08.09 |
[Spring] DI, IoC 컨테이너 (0) | 2023.08.03 |
[Spring] SOLID 원칙에 대해 알아보자 (0) | 2023.08.03 |