개요
- 프록시를 이용하면 기존로직에 변화를 주지 않고도 새로운 기능을 추가할 수 있다.
- 스프링은 이러한 방법을 이용해서 유연하게 기능을 확장했다.
- 그래서 기존코드를 건드리지 않고 새로운 기능을 추가할 수 있는 프록시 패턴과 데코레이터 패턴을 학습하려고 한다.
클라이언트, 서버, 프록시
- 보통 Client와 Server를 생각하면 컴퓨터 네트워크의 Client, Server 를 많이 떠올리지만, 이 개념을 객체에 도입하면 요청하는 객체는 클라이언트가 되고, 요청을 처리하는 객체는 서버가 된다.
- 하지만 여기서 Client가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자를 통해서 간접적으로 서버에 요청할 수 있다.
- 여기서 그 대리자가 바로 프록시다.
- 프록시는 객체안에서의 개념도 있고, 웹 서버에서의 개념도 있지만 규모의 차이가 있을 뿐 근본적인 역할은 같다.
프록시가 되기 위한 조건
- Client는 Server에게 요청을 한 것인지, Proxy에게 요청을 한 것인지 몰라야 한다. -> 서버객체를 프록시로 변경해도 클라이언트의 코드는 변하지 않아야 한다.
- 그러기 위해서 Server와 Proxy는 같은 인터페이스를 구현해야한다.
프록시의 주요기능
- 프록시를 통해서 할 수 있는 일은 크게 2가지로 구분할 수 있다.
- 접근 제어
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
- 부가 기능 추가
- 접근 제어
프록시 패턴
- 프록시 패턴은 위의 프록시 기능중 접근 제어를 하는 기능을 구현하는데 사용되는 패턴이다.
- 이번에는 그 중에서 캐싱에 해당되는 기능을 프록시 패턴으로 구현해 보려고 한다.
- 프록시 패턴에서 말하는 프록시는 위에서 안급했던 프록시의 개념과는 다르다.
프록시 패턴 적용 전
public interface Subject {
String operation();
}
@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();
}
}
}
- RealSubject는 Server에 해당한다.
- 위와 같이 "data"를 반환하기 전에 1초를 쉰다. -> 데이터를 받아오는 시간이라고 생각하면 된다.
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
- ProxyPatternClient는 Client에 해당한다.
public class ProxyPatternTest {
@Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
}
}
- 위와 같이 Client는 Server를 직접 주입 받는다.
- 위의 코드를 실행시키면 총 3초의 시간이 소요된다.
- 이제 프록시패턴을 사용해서 캐싱기능을 구현해 3초에서 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;
}
}
- 위와 같이 Server에 해당하는 RealSubject와 같은 인터페이스(Subject)를 구현해야한다.
- 그리고 Proxy가 Server에 접근할 수 있어야 하기때문에 상위 인터페이스(Subject)를 참조하고 있어야한다.
- operation을 보면, 값이 없으면 Server에 접근하고 있으면 Proxy에 저장된 값을 반환한다. -> 캐싱 기능
@Test
void cacheProxyTest() {
Subject realSubject = new RealSubject();
Subject cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
- 위와 같이 Proxy에 Server를 주입한다.
- 그 다음 클라이언트는 Server가 아닌 Proxy를 주입 받는다.
- 실행 결과는 처음 execute만 Proxy가 Server에 접근하고 나머지는 Proxy에 저장된 값이 반환되기 때문에 약 1초의 시간만 소요된다.
- 이렇듯 RealSubject 코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근 제어할 수 있다.
- 또한, 라이언트 코드의 변경 없이 자유롭게 프록시를 넣고 뺄 수 있다. 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알지 못한다.
데코레이터 패턴
- 다음으로는 데코레이터 패턴을 적용해 보려고한다.
- 위에서 배웠던 프록시 패턴과 마찬가지로 프록시를 사용한다는 공통점이 있다.
- 하지만 프록시 패턴이 접근을 제어하는 것이 초점을 맞췄다면, 데코레이터 패턴은 객체에 추가 기능을 부여하는데 주로 사용된다.
데코레이터 패턴 적용 전
public interface Component {
String operation();
}
@Slf4j
public class RealComponent implements Component {
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
@Slf4j
public class DecoratorPatternClient {
private Component component;
public DecoratorPatternClient(Component component) {
this.component = component;
}
public void execute() {
String result = component.operation();
og.info("result={}", result);
}
}
- Client 코드는 Component 인터페이스에 의존하며, component.operation()를 실행한다.
@Slf4j
public class DecoratorPatternTest {
@Test
void noDecorator() {
Component realComponent = new RealComponent();
DecoratorPatternClient client = new DecoratorPatternClient(realComponent);
client.execute();
}
}
- 위와 같이 Client가 Component를 주입 받아 excute()하는 것을 확인할 수 있다.
- 이제 데코레이터 패턴을 사용해서 부가기능(로그)을 코드의 변경없이 구현해 보도록 하겠다.
데코레이터 패턴 적용 후
@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;
}
}
- 위와 같이 로그를 찍는 부가 기능을 구현했다.
- 실제 구현체와 같이 인터페이스를 구현해야 하기 때문에 Component를 구현하고 있다.
- 또한 실제 구현체를 실행해야하기 때문에 Component를 변수로 가지고 있다.
- 로그를 찍으면서 component.operation();를 실행시켜 실제 구현체를 호출하는 것을 확인할 수 있다.
@Test
void decorator1() {
Component realComponent = new RealComponent();
Component messageDecorator = new MessageDecorator(realComponent);
DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
client.execute();
}
- 위의 코드를 보면, 실제 구현체(realComponent)를 messageDecorator(프록시, 부가기능)에 주입해준다.
- 또한 Client는 더이상 실제 구현체가 아닌 프록시에 의존한다.
- 의존관계를 정리하면, client -> messageDecorator -> realComponent 가 되는 것이다.
- client.execute 가 실행되면 MessageDecorator.operation()이 실행되며, MessageDecorator.operation() 내부에서 부가기능을 수행하고 realComponent.operation()을 실행하게 된다.
- 즉 기존 코드를 수정하지 않고 부가기능을 추가할 수 있었다.
- 만약 실행시간을 측정하는 부가기능을 하나 더 추가한다면?
@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;
}
}
- 위와 같이 실행시간을 측정하는 부가기능을 추가한다.
- 이 클래스 또한 Component를 구현하고 있으며, Component를 변수로 가지고 있는것을 확인할 수 있다.
@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가 실행된다.
- 그리고 마지막으로 실제 구현체인 realComponet가 실해된다.
- 따라서 의존관계는 client -> timeDecorator -> messageDecorator -> realComponent 가 된다.
정리
- 프록시 패턴과 데코레이터 패턴은 거의 비슷한 것을 확인할 수 있다.
- 하지만, 의도에서 차이가 있다.
- 접근을 제어하기 위한 의도라면 프록시 패턴을, 추가 기능을 동적으로 추가하고 싶다면 데코레이터 패턴을 사용하면 된다.
- 이 둘 모두 코드의 변경없이 어떠한 역할을 추가하기 위해 생긴 디자인 패턴이며 때로는 둘이 완전히 같을 때도 있다.
References
- 김영한님의 스프링 고급편
'객체지향' 카테고리의 다른 글
프록시 적용하기 (0) | 2022.08.06 |
---|---|
생성 패턴 (디자인 패턴) (0) | 2022.05.15 |
행위 패턴 (디자인 패턴) (0) | 2022.05.15 |
구조패턴 (디자인 패턴) (0) | 2022.05.15 |
클래스 다이어그램 (0) | 2022.04.23 |