요청자를 Client 요청을 처리해주는 자를 Server라고 할 때,
Client에서 Server에 처리를 요청하는 것은 직접 요청이고
Client에서 어떤 대리자를 거쳐서 Server에 처리를 요청하는 것은 간접 요청이다.
간접 요청에서 이 대리자를 프록시(Proxy)라고 한다.
간접요청을 하면 프록시를 통해 여러가지 일을 할 수 있다.
접근 제어, 부가 기능을 추가할 수 있고 대리자를 통해 또 다른 대리자를 호출할 수도 있다(프록시 체인).
접근 제어 (프록시 패턴)
권한에 따른 접근 차단
캐싱
지연 로딩
부가 기능 추가(데코레이터 패턴)
원래 서버가 제공하는 기능에 더해서 부가 기능을 수행한다.
객체에서 프록시가 되려면, Client는 Server에 요청을 한 건지, 프록시에 요청을 한 것인지 조차 몰라야한다. (대체 가능)
-> 프록시와 실제 객체가 같은 인터페이스를 사용해야한다.
-> Client가 사용하는 Server 객체를 프록시 객체로 변경해도 Client 코드를 수정할 필요가 없어야 한다.
// 실제 Server 객체
@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();
}
}
}
// 프록시 객체
@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;
}
}
// Client 객체
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject){
this.subject = subject;
}
public void execute(){
subject.operation();
}
}
// 테스트
@Test
public void cacheProxyTest() throws Exception{
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
// 결과
17:33:29.814 [main] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
17:33:29.819 [main] INFO hello.proxy.pureproxy.proxy.code.RealSubject - 실제 객체 호출
17:33:30.820 [main] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
17:33:30.821 [main] INFO hello.proxy.pureproxy.proxy.code.CacheProxy - 프록시 호출
실제 객체에서 데이터를 가져오는데 1초가 걸린다고 가정하면 프록시가 없을 경우엔 3초가 걸렸을 것이다.
그러나 프록시 객체에서 캐싱을 통해 접근제어를 한다면 처음 1회 후 나머지 2회는 캐시에서 반환한다.
따라서 1초가 된다.
//실제 Server 객체
@Slf4j
public class RealComponent implements Component{
@Override
public String operation() {
log.info("RealComponent 실행");
return "data";
}
}
//데코레이터 객체
@Slf4j
public class MessageDecorator implements Component{
private RealComponent target;
public MessageDecorator(RealComponent target) {
this.target = target;
}
@Override
public String operation() {
log.info("MessageDecorator 실행");
String result = target.operation();
String decoResult = "*****" + result + "*****";
log.info("적용 전 = {}, 적용 후 = {}", result, decoResult);
return decoResult;
}
}
//Client 객체
@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
public void decorator1() throws Exception{
RealComponent realComponent = new RealComponent();
MessageDecorator messageDecorator = new MessageDecorator(realComponent);
DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
client.execute();
}
// 결과
17:48:59.170 [main] INFO MessageDecorator - MessageDecorator 실행
17:48:59.173 [main] INFO RealComponent - RealComponent 실행
17:48:59.183 [main] INFO MessageDecorator - 적용 전 = data, 적용 후 = *****data*****
17:48:59.185 [main] INFO DecoratorPatternClient - result = *****data*****
프록시 패턴과 차이점은 목적이 접근 제어냐 부가 기능 추가냐의 차이가 있고 형태는 동일하다.
상속을 사용해도 다형성에 의해 프록시 객체를 사용할 수 있다.
다만, 상속에 따른 제약조건이 따라온다
// 실제 Server 객체
@Slf4j
public class ConcreteLogic {
public String operation(){
log.info("ConcreteLogic 실행");
return "data";
}
}
// 프록시 객체
@Slf4j
public class TimeProxy extends ConcreteLogic{
private ConcreteLogic concreteLogic;
public TimeProxy(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = concreteLogic.operation();
long endTime = System.currentTimeMillis();
log.info("TimeDecorator 종료 ResultTime = {}ms", endTime-startTime);
return result;
}
}
// Client 객체
@Slf4j
public class ConcreteClient {
private ConcreteLogic concreteLogic;
public ConcreteClient(ConcreteLogic concreteLogic) {
this.concreteLogic = concreteLogic;
}
public void execute(){
concreteLogic.operation();
}
}
// Test
@Test
public void addProxy() throws Exception{
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic);
ConcreteClient concreteClient = new ConcreteClient(timeProxy);
concreteClient.execute();
}
// 결과
18:55:27.366 [main] INFO TimeProxy - TimeDecorator 실행
18:55:27.370 [main] INFO ConcreteLogic - ConcreteLogic 실행
18:55:27.371 [main] INFO TimeProxy - TimeDecorator 종료 ResultTime = 0ms
기존 코드를 건드리지 않고 원하는 기능을 추가할 수 있는 것은 큰 장점이다.
그러나 프록시를 적용할 클래스를 하나하나 전부 만들어줘야 하는 것은 문제점이다.
이러한 점을 개선한 것으로 동적 프록시 기술이 있다.
다음 포스트에 소개하겠습니다.
잘 보고갑니다!