프록시
- 클라이언트의 요청과 서버의 응답 사이에서 요청과 응답에 대한 대리자로써 역할을 하는 것.
- 요청과 응답 사이에서 대리인으로써 서로 직접 요청, 응답하지 않고 프록시를 통하여 요청, 응답한다.
GoF 디자인 패턴
GoF 디자인 패턴에서 프록시 패턴과 데코레이터 패턴은 모두 프록시를 사용하는 방법이다.
GoF 디자인 패턴에서는 이 둘을 의도(Intent)에 따라서 프록시 패턴과 데코레이터 패턴으로 구분한다.
둘다 프록시를 사용하지만, 의도가 다르다는 점이 핵심이다. 용어가 프록시 패턴이라고 해서 이 패턴만 프록시를 사용하는 것은 아니다.
- 직접 호출
클라이언트와 서버 개념에서 일반적으로 클라이언트가 서버를 호출하고, 처리 결과를 직접 받는다.
- 간접 호출
클라이언트가 요청한 결과를 서버에 직접 요청하는 것이 아니라 어떤 대리자(proxy)를 통해서 대신 간접적으로 서버에 요청할 수 있다.
ex> 직접 장을 볼 수 있지만, 누군가에게 대신 장을 봐달라고 부탁할 수 있다. 여기서 대신 장을 보는 대리자를 proxy라 할 수 있다.
직접 호출과 다르게 간접 호출을 하면 대리자가 중간에서 여려가지 일을 할 수 있다.
요약하면 프록시 객체로 중간에서 접근 제어와 부가 기능 추가를 수행할 수 있다.
위 사진처럼 Client는 ServerInterface만 의존하여, 구현 클래스와는 의존성을 분리하여 사용한다.
또, 서버와 프록시는 같은 인터페이스를 사용하기 때문에, DI를 사용하여 대체 가능하다.
//ServerInterface
public interface Subject {
String operation();
}
//Server
@Slf4j
public class RealSubject implements Subject{
@Override
public String operation() {
log.info("실제 객체 호출");
sleep(1000); //데이터 처리 1초 가정
return "data";
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
//Client
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute(){
subject.operation();
}
}
//Proxy
@Slf4j
public class CacheProxy implements Subject{
private Subject target; //프록시가 호출되는 대상
private String cacheValue;
public CacheProxy(Subject target) {
this.target = target;
}
/**
* 클라이언트가 프록시를 호출하면 프록시가 최종적으로 실제 객체를 호출해야 한다.
* 따라서 내부에 실제 객체의 참조를 가지고 있어야 한다.
* 이렇게 프록시가 호출하는 대상을 target이라 한다.
*
* 처음 호출 이후에 매우 빠르게 호출할 수 있음.
* @return
*/
@Override
public String operation() {
log.info("프록시 호출");
if ( cacheValue == null ) //cacheValue에 값이 없으면 실제 객체 호출
cacheValue = target.operation();
return cacheValue;
}
}
위 코드에서 Subject라는 인터페이스를 구현하여 서버 객체, 프록시 객체를 구현하여 사용하고 있다.
클라이언트는 Subject만 의존하기 때문에, 서버 객체인지 프록시 객체인지 알 필요가 없기에
런타임 시에 DI를 통해 Server에서 Proxy로 변경되어도 클라이언트에서는 변경이 전혀 일어나지 않는다.
위의 GoF 디자인 패턴 설명에서 얘기했듯이,
프록시 패턴의 주요 기능은 접근 제어이다.
캐시 도 접근 자체를 제어하는 기능 중 하나이다.
ex > 접근 시 데이터가 있을 경우 캐시 데이터 반환, 없을 시 서버 호출 후 반환
public class ProxyPattrernTest {
@Test
void noProxyTest(){
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
//client 3번 호출, 3초 소요
client.execute();
client.execute();
client.execute();
}
/**
* Cache Proxy 사용
* CacheProxy에서 데이터가 없을 경우 RealSubject를 사용하고,
* 데이터가 있을 경우 Proxy에서 바로 반환
*/
@Test
void cacheProxyTest(){
RealSubject realSubject = new RealSubject();
CacheProxy cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
//client 3번 호출, 1초 소모
client.execute();
client.execute();
client.execute();
}
}
realSubject와 ProxyPatternClient를 생성하고 둘을 연결한다.
client -> Server
런타임 객체 의존 관계가 완성된다.
noProxyTest()의 경우 실제 서버를 3번 호출한다.
Server에서는 데이터 처리 1회당 1초의 시간을 소요하므로 3초의 시간이 소요된다.
이 과정을 통해서
client -> cacheProxy -> realSubject
런타임 객체 의존 관계가 완성된다.
cacheProxtTest()는 client.execute()를 총 3번 호출한다.
하지만 실제 realSubject를 호출하지 않고 cacheProxy를 호출한다.
처리
1. client의 cacheProxy 호출 -> cacheProxy에 데이터 없음 -> realSubject 호출
2. client의 cacheProxy 호출 -> cacheProxy에 데이터 있음 -> cacheProxy에서 즉시 반환
3. client의 cacheProxy 호출 -> cacheProxy에 데이터 있음 -> cacheProxy에서 즉시 반환
결과적으로 캐시 프록시를 도입함으로써 최초 1번만 실제 서버에 요청하고, 이 후에는 프록시에서 캐싱을 통해 데이터를 반환하므로 성능상 효율이 상당히 개선된다.
프록시 패턴의 핵심
RealSubject 코드와 클라이언트 코드를 전혀 변경하지 않고, 프록시를 도입해서 접근을 제어한다.