이전에 템플릿 메서드 패턴과 콜백 패턴을 사용하여 부가기능을 추가해 보았다.
코드 수정을 최소화하기는 했지만, 부가기능을 적용해야 할 클래스가 증가와 수정사항이 발생할 경우 일일이 원본 코드를 변경하는 문제점이 남아있었다.
원본 코드를 수정하지 않고 부가기능을 적용해기 위해서는 프록시라는 개념을 알아야 한다.
클라이언트와 생각해 보면 클라이언트는 '의뢰인'이라는 뜻을 가지고 서버는 '서비스나 상품을 제공하는 사람이나 물건을 의미한다'. 이를 웹개발 측면에서 보면 클라이언트는 브라우저 서버는 웹 서버가 된다.
일반적으로 클라이언트가 서버를 직접 호출하면 서버는 결과를 반환해준다.
클라이언트가 요청한 결과를 서버에 직접 요청하지 않고 대리자를 통해 대신 간접적으로 요청할 수 있다. 이 때 대리자를 영어로 프록시(Proxy)라 한다.
이 경우 대리자는 다양한 역할을 수행할 수 있다.
객체에서 프록시가 되려면, 클라이언트는 서버에게 요청을 한 것인지, 프록시에게 요청을 한 것인지 조차 몰라야 한다.
즉, 서버와 프록시는 같은 인터페이스를 사용해야 한다. 그리고 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다.
클래스 의존관계를 보면 클라이언트는 서버 인터페이스( ServerInterface
)에만 의존한다. 그리고 서버와 프록시가 같은 인터페이스를 사용한다. 따라서 DI를 사용해서 대체 가능하다.
런타임 객체 의존관계를 보면, client ➡ server 을 DI를 이용해 client ➡ proxy로 변경하면 크라이언트 코드 변경없이 의존관계 변경이 가능하다.
프록시를 사용하는 패턴은 프록시 패턴과 데코레이터 패턴이 있다. 이 두 패턴은 거의 비슷해서 사용하는 의도(intent)로 구분하자.
프록시 패턴 (접근제어가 목적)
데코레이터 패턴(부가기능 추가 목적)
❗ 프록시 패턴만 프록시를 사용하는 게 아니다.의도로 구분하자!!
public interface Subject {
String operation();
}
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();
}
}
}
코드를 입력하세요
operation()
재정의하여 실제 데이터 호출시 1초가 걸린다고 가정public class ProxyPattenClient {
private Subject subject;
public ProxyPattenClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
Subject
인터페이스에 의존하여 실제 호출하는 클라이언트 코드이다. @Test
void noProxyTest() {
RealSubject realSubject = new RealSubject();
ProxyPattenClient client = new ProxyPattenClient(realSubject);
client.execute();
client.execute();
client.execute();
}
client.execute()
를 3번 호출하여 총 3초가 걸렸다. 프록시 패턴을 통해 캐시를 적용해보자.
@Slf4j
public class CacheProxy implements Subject {
private Subject target;
private String cacheValue; // cacheValue 값 존재하면 그 target 호출 않고 cacheValue 반환
public CacheProxy(Subject target) {
this.target = target;
}
@Override
public String operation() {
log.info("프록시 호출");
if (cacheValue == null) {
cacheValue = target.operation();
}
return cacheValue;
}
}
private Subject target
클라리언트가 프록를 호출하면 프록시는 실제 객체를 호출해야하기 때문에 실제 객체의 참조를 가져야 한다. 실제객체를 target이라 함 operation()
코드를 보면 cacheValue
가 null
인 경우에만 원본 객체를 호출하고, 값이 존재하면 cacheValue
를 반환한다. @Test
void cacheProxyTest() {
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPattenClient client = new ProxyPattenClient(cacheProxy);
client.execute();
client.execute();
client.execute();
}
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();
log.info("result={}",result);
}
}
Component
인터페이스에만 의존한다. @Test
void noDecorator() {
Component realComponent = new RealComponent();
DecoratorPatternClient client = new
DecoratorPatternClient(realComponent);
client.execute();
}
client ➡ realComponent
의 의존관계를 가지고 있다. 프록시로 부가기능을 추가하는 것을 데코레이터 패턴이라 한다.
대표적으로 로그를 추가하거나 요청값, 응답값을 중간에 변형하는 등의 기능이 있다.
@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
에 저장한다. operation()
호출시 프록시와 연결된 대상을 호출하고, 그 응답 값에 ****
을 더해 꾸민 후 반환한다. @Test
void decorator1() {
RealComponent realComponent = new RealComponent();
MessageDecorator messageDecorator = new MessageDecorator(realComponent);
DecoratorPatternClient client = new DecoratorPatternClient(messageDecorator);
client.execute();
}
client -> messageDecorator -> realComponent
의 객체 의존 관계 생성 실행 시간 측정하는 기능을 추가해 보자
@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() {
RealComponent realComponent = new RealComponent();
MessageDecorator messageDecorator = new MessageDecorator(realComponent);
TimeDecorator timeDecorator = new TimeDecorator(messageDecorator);
DecoratorPatternClient client = new DecoratorPatternClient(timeDecorator);
client.execute();
}
실행 결과
지금까지 인터페이스를 통해 프록시를 적용했다.
만약 인터페이스가 없는 구체 클래스를 프록시로 적용하려면 어떻게 해야 할까 ?
➡ 자바의 다형성은 인터페이스를 구현하든 , 클래스를 상속하든 상위 타입만 맞으면 다형성이 적용된다. 즉, 클래스의 상속을 이용한 프록시가 가능하다.
@Slf4j
public class ConcreteLogic {
public String operation() {
log.info("ConcreteLogic 실행");
return "data";
}
}
@Slf4j
public class TimeProxy extends ConcreteLogic {
private ConcreteLogic realLogic;
public TimeProxy(ConcreteLogic realLogic) {
this.realLogic = realLogic;
}
@Override
public String operation() {
log.info("TimeDecorator 실행");
long startTime = System.currentTimeMillis();
String result = realLogic.operation();
long endTime = System.currentTimeMillis();
long resultTime = endTime - startTime;
log.info("TimeDecorator 종료 resultTime={}", resultTime);
return result;
}
}
@Test
void addProxy() {
ConcreteLogic concreteLogic = new ConcreteLogic();
TimeProxy timeProxy = new TimeProxy(concreteLogic);
ConcreteClient client = new ConcreteClient(timeProxy);
client.execute();
}
프록시를 사용하고 해당 프록시가 접근 제어가 목적이라면 프록시 패턴이고, 새로운 기능을 추가하는 것이 목적이라면 데코레이터 패턴이 된다.