0. 프록시란?
클라이언트 (client) 와 서버 (server) 개념에서 클라이언트는 서버에 필요한 것을 요청하고, 서버는 그 요청을 처리하는 것이다.
네트워크 개념으로 생각하면, 클라이언트는 웹브라우저가 되고 서버는 웹 서버이다. 객체로 생각하면 요청하는 객체가 클라이언트이고 요청을 처리하는 객체가 서버이다.
이 때, 클라이언트가 서버를 직접 호출하고 처리 결과를 직접 받는다면 직접 호출이라하고, 어떤 대리자를 통해서 대신 간접적으로 요청하고 결과를 받는다면 간접 호출이라 한다. 여기서 대리자를 프록시 (Proxy) 라고 한다.
그런데, 대리자를 이용하면 그 대리자가 중간에 여러가지 일을 할 수 있다는 점이 특징이다. 대리자에게 "커피 좀 사와"라고 부탁했는데, 그 대리자가 이미 커피가 준비되어 있다며 사러가지 않고도 바로 전달해줄수도 있고, 다른 대리자에게 다시 커피 사오라고 재요청할 수도 있는 등 여러가지 일을 할 수 있다. 실제 프록시가 이렇게 동작할 수 있다.
0.1. 객체관점에서 프록시가 되려면?
객체에서 프록시가 되려면 클라이언트는 서버에게 요청한 것인지, 프록시에게 요청한 것인지 몰라야 한다.
즉 서버와 프록시는 같은 인터페이스를 사용해야 한다. 클라이언트가 사용하는 서버 객체를 프록시 객체로 변경해도 클라이언트 코드를 변경하지 않고 동작할 수 있어야 한다는 의미이다.
클라이언트는 서버인터페이스만 의존하고, 서버와 프록시가 같은 서버인터페이스를 사용하면 DI 를 통해 서버 객체를 프록시 객체로 대체할 수 있다.
런타임 객체 의존 관계로 보면, 런타임시점에 클라이언트 객체에 DI 를 사용해서 Clinet → Server 에서 Client → Proxy 로 객체 의존관계를 변경해도 클라이언트 코드를 변경하지 않아도 된다. 클라이언트 입장에서는 프록시로 변경했는지 모른다. DI 를 통해 클라이언트 코드의 변경없이 유연하게 프록시를 주입할 수 있다.
0.2. 프록시의 주요 기능
프록시를 통해서 할 수 있는 일은 크게 접근 제어와 부가 기능 추가로 구분할 수 있다.
- 접근 제어
- 권한에 따른 접근 차단
- 캐싱
- 지연 로딩
- 부가 기능 추가
- 원래 서버가 제공하는 기능에 더해서 부가 기능을 추가한다.
- 예) 요청 및 응답 결과를 중간에 가공하는 것
- 예) 실행 시간 측정으로 추가 로그 남기는 것
GOF 디자인 패턴에서는 이 둘을 의도 (intent) 에 따라서 프록시 패턴과 데코레이터 패턴으로 구분한다.
- 프록시 패턴 : 접근 제어가 목적
- 데코레이터 패턴 (링크) : 부가 기능 추가가 목적
💡 프록시 패턴 (proxy pattern)
어떤 객체에 대한 접근을 제어하기 위한 용도로, 대리인이나 대변인에 해당하는 객체를 제공하는 패턴이다.
프록시 패턴을 사용하면 원격 객체라든가 생성하기 힘든 객체, 보안이 중요한 객체와 같은 다른 객체에 대한 접근을 제어하는 대변자 객체를 만들 수 있다.
1. 예제
1.1. 프록시를 도입하기 전 코드
1. Subject 인터페이스
// Subject 인터페이스
public interface Subject {
String operation();
}
2. RealSubject 구현 객체
// RealSubject : Subject 인터페이스를 구현한 객체
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RealSubject implements Subject {
@Override
public String operation() {
log.info("실제 객체 호출");
// 데이터 조회를 시뮬레이션 하기위해 1초 쉬도록 한다.
sleep(1000); // DB 조회에 1초 걸린다는 가정
return "data";
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
3. Client 객체
// Client 객체
public class ProxyPatternClient {
private Subject subject;
public ProxyPatternClient(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.operation();
}
}
4. 실행
// 실행코드 : 테스트코드 (src/test) 이용
import org.junit.jupiter.api.Test;
class ProxyPatternTest {
@Test
void noProxyTest() {
// 프록시 도입 전 : 실제 서버를 호출하여 처리한다.
RealSubject realSubject = new RealSubject();
ProxyPatternClient client = new ProxyPatternClient(realSubject);
client.execute();
client.execute();
client.execute();
// 각 호출마다 1초씩 소요되므로 3초 이상 걸린다.
// 만약 모두 같은 데이터를 호출한다면? 캐시해두는 것이 성능상 좋다.
}
}
5. 결과
각 호출 (client.execute() 메소드) 마다 1초씩 소요되므로 총 3초 이상 걸린다. 만약, 모두 같은 데이터를 호출한다면 캐시해두었다가 값을 가져오는 것이 성능상 좋다.
1.2. 프록시 도입 코드
프록시 도입 전 코드에서 프록시를 추가한다.
1. CacheProxy 프록시 추가
// 프록시 객체 : 처리서버(RealSubject) 와 같은 인터페이스(Subject) 를 구현한다.
import lombok.extern.slf4j.Slf4j;
@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;
}
}
2. 실행 : ProxyPatternTest 클래스에 cacheProxyTest() 테스트케이스 추가
import org.junit.jupiter.api.Test;
class ProxyPatternTest {
/**
* RealSubject 코드와 client 코드를 변경하지 않고, 프록시 도입으로 접근 제어를 했다.
* 클라이언트 코드의 변경 없이 프록시를 넣고 뺄 수 있다.
* 실제 클라이언트 입장에서는 프록시 객체가 주입되었는지, 프록시가 주입되었는지 알 수 없다.
*/
@Test
void cacheProxyTest() {
Subject realSubject = new RealSubject();
Subject cacheProxy = new CacheProxy(realSubject);
ProxyPatternClient client = new ProxyPatternClient(cacheProxy);
client.execute();
client.execute();
client.execute();
// 캐시된 값을 가져오므로 noProxyTest() 보다 성능성 더 좋다
}
}
3. 결과 확인
프록시 도입 전 noProxyTest() : 각 호출마다 1초가 소요되므로 3초 이상 걸린다.
프록시 도입 후 cacheProxyTest() : 값이 있으면 캐시에서 가져오므로 실제 호출은 1번만 수행되어 약 1초 걸린다.
2. 정리
프록시 도입 과정으로 정리해보자. 클라이언트 (Client) 코드와 실제 서버 (RealSubject) 코드를 변경하지 않고 CacheProxy 를 도입한 것 만으로 접근 제어를 할 수 있었다. 클라이언트의 코드 변경 없이 프록시를 자유롭게 설정할 수 있었다.
즉, 클라이언트 입장에서 프록시 객체가 주입되었는지, 실제 객체가 주입되었는지 알 수 없다.
이러한 프록시 패턴에는 크게 3가지 방식이 존재한다.
● 프록시에서 접근을 제어하는 방법
- 원격 프록시 (Remote Proxy) 를 써서 원격 객체에 대한 접근을 제어할 수 있다.
- 가상 프록시 (Virtual Proxy) 를 써서 생성하기 힘든 자원에 대한 접근을 제어할 수 있다.
- 보호 프록시 (Protection Proxy) 를 써서 접근 권한이 필요한 자원에 대한 접근을 제어할 수 있다.