프록시 패턴이란 특정 객체로의 접근을 제어하는 대리인(특정 객체를 대변하는 객체)을 제공하는 패턴이다. 객체에 대한 접근을 제어하는 이유가 무엇일까? 이유는 특정 객체에 접근하여 메인 로직 이전이나 이후에 어떤한 기능을 수행하기 위해서는 해당 객체의 클래스를 수정해야할 수 있다. 그러나 프록시를 이용한다면 원래 클래스의 코드를 수정하지 않고 프록시를 이용하여 구현할 수 있다.
이는 프록시와 특정 객체를 하나 묶어주는 역할을 하는 인터페이스이다.
원본의 객체를 의미한다.
클라이언트와 대상 객체(Service)를 중계할 대리인 역할을 한다. 프록시는 대상 객체를 합성하여 사용하며 대상 객체와 같은 이름의 메스드를 호출하며, 별도의 로직을 수행할 수 있다. 별도의 로직이란 클라이언트가 대상 객체를 이용하기 전후에 실행할 로직이라고 할 수 있다.
ServiceInterface를 이용하여 프록시 객체를 생성해 이용하는 역할이다.
프록시 패턴을 이용한다면 기존 대상 객체의 코드를 변경하지 않고 새로운 기능을 추가할 수 있다. 이는 데코레이터 패턴과 유사하다. 기능 확장에 필요한 새로운 기능을 추가하기 위해서 대상 객체의 코드는 수정하지 않고 Proxy 인터페이스에 새로운 객체의 인터페이스를 호출하는 방법으로 추가가 가능하다.
대상 객체는 자신의 책임에만 집중하고, 그 이외의 부가 기능을 제공하는 역할은 프록시 객체에 위임함으로 단일 책임 원칙을 위반하지 않는다. 접근 권한이나 생성에 대한 부분을 프록시에게 모두 위임하고 자신이 가지는 단일 책임만 수행한다.
프록시를 이용하여 기능을 확장하는 것은 내 생각에는 별로 좋지는 않은 것 같다. 왜냐하면 원본 객체의 메인 로직 전후에 기능을 추가하는 것이지 메인 기능을 확장한다는 의미로 사용되지는 않는것 같기 때문이다.
따라서 서비스 클래스를 사용하지 직전이나 직후에 지연 로딩 방식이나 로깅 같은 접근 권한 확인, 파라미터 유효성 검사와 같은 케이스에서 사용하는 것이 올바른 프록시 패턴 사용이라고 생각이 든다. 실무에서는 전문 API 통신시 사용된다고 한다. 저문을 만들고 전문 API 서버로 직접 통신하는 방식이 아니라 프록시 서버를 통해서 인증 절차를 진행한 후에 요청한 전문이 정상적으로 전문 API 서버로 전달되고 응답을 받는다고 한다.
대상 객체의 메서드를 직접 클라이언트가 호출하는 것이 아니라, 대상 객체에 접근하기 전에 프록시 객체의 메서드를 접근 한 후 추가적인 로직을 처리한 뒤 접근하게 한다.
//Client
public class Client {
private final Subject subject;
public Client(Subject subject) {
this.subject = subject;
}
public void execute() {
subject.action();
}
}
//Subject interface
public interface Subject {
void action();
}
//Concrete Subject
public class RealSubject implements Subject{
@Override
public void action() {
System.out.println("RealSubject action");
}
}
//Proxy
public class Proxy implements Subject {
private final RealSubject realSubject;
public Proxy(RealSubject realSubject) {
this.realSubject = realSubject;
}
public void action(){
realSubject.action();
System.out.println("Proxy action");
}
}
@Test
void 기본형_프록시_테스트(){
RealSubject realSubject = new RealSubject();
Proxy proxy = new Proxy(realSubject);
Client client = new Client(proxy);
client.execute();
}
//결과
RealSubject action
Proxy action
지연 초기화 방싱을 사용할 때 적용하며 가끔 필요하지만 항상 메모리에 적재되어 있는 무거운 서비스 객체가 있느 경우에 사용된다. 이 방식은 실제 객체의 생성에 많은 자원이 소모 되지만 사용 빈도는 낮을 때 사용되며 서비스가 시작될 때 객체를 생성하는 대신 객체 초기화가 실제로 필요한 시점에 초기화될 수 있도록 지연할 수 있다.
public class VirtualProxy implements Subject {
private RealSubject realSubject;
public VirtualProxy(){
}
public void action(){
if(realSubject == null){
realSubject = new RealSubject();
System.out.println("VirtualProxy init RealSubject");
}
realSubject.action();
System.out.println("VirtualProxy action");
}
}
@Test
void 가상_프록시_테스트(){
VirtualProxy virtualProxy = new VirtualProxy();
System.out.println("가상 프록시 생성");
Client client = new Client(virtualProxy);
client.execute();
}
//결과
가상 프록시 생성
VirtualProxy init RealSubject
RealSubject action
VirtualProxy action
결과를 보면 알 수 있듯이 Proxy 생성 시점에 Subject가 초기화되는 것이 아닌 프록시가 대상 객체의 메서드 호출 시점에 초기화가 된다.
프록시가 대상 객체에 대한 자원으로 액세스 제어를 하기 위해 사용된다. 특정 클라이언트만 서비스 객체를 사용할 수 있도록 접근 권한을 검증한다. 클라이언트의 자격 증명이 기준과 일치하는 경우에만 서비스 객체에 요청을 전달 할 수 있다.
public class ProtectionProxy implements Subject {
private final RealSubject realSubject;
private final boolean access;
public ProtectionProxy(RealSubject realSubject, boolean access) {
this.realSubject = realSubject;
this.access = access;
}
public void action(){
if(access){
realSubject.action();
System.out.println("ProtectionProxy action");
}else{
System.out.println("ProtectionProxy no access");
}
}
}
// 테스트
@Test
void 보호_프록시_테스트_접근_허용(){
RealSubject realSubject = new RealSubject();
ProtectionProxy protectionProxy = new ProtectionProxy(realSubject, true);
Client client = new Client(protectionProxy);
client.execute();
}
//결과
RealSubject action
ProtectionProxy action
@Test
void 보호_프록시_테스트_접근_거부(){
RealSubject realSubject = new RealSubject();
ProtectionProxy protectionProxy = new ProtectionProxy(realSubject, false);
Client client = new Client(protectionProxy);
client.execute();
}
//결과
ProtectionProxy no access
접근 권한 검증을 추가 기능으로 추가하여 접근 권한이 없으면 대상 객체의 접근할 수 없다.
로킹 프록시는 말 그대로 로그를 남기기 위한 프록시이다. 대상 객체의 메서드를 실행하기 전이나 후에 로깅을 하는 기능을 추가하여 재정의한다.
프록시 클래스는 로컬에 존재하고 대상 객체는 원격에 존재하는 경우 네트워크를 통해 클라이언트의 요청을 전달하여 네트워크와 관련된 불필요한 작업들을 처리하고 결과값만 반환한다.
데이터가 큰 경우 프록시를 이용하여 캐싱하여 재사용을 유도한다. 클라이언트 요청의 결과를 캐시하고 이 캐시의 수명 주기를 관리한다. Http Proxy와 같은 것이 캐싱 프록시라 할 수 있다.
예제 코드
참고: https://refactoring.guru/ko/design-patterns/proxy
참고: https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%94%84%EB%A1%9D%EC%8B%9CProxy-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90