프록시 패턴은 GoF패턴 중에 구조 패턴에 속하는 디자인 패턴이다. NodeJS에서 promise, JPA에서 LazyLoading 등의 기법에서 사용이 된다. promise는 실질적인 반환 값이 나오기 전에 동기적으로 promise 객체를 반환하기 위해 이 프록시 패턴이 사용되고, Lazyloading도 마찬가지로 직접 연관된 객체에 접근하기 전까지 추가적인 query 발생을 최소화하기 위해 사용된다는 점을 알아두고 살펴보자.

프록시 패턴은 원래 자신이 처리해야하는 업무를 다른 객체에게 위임하여 대신 처리하게끔 하는 패턴이다.
다만, 어댑터 패턴과 달리, 본인이 처리를 하지 못하는 업무를 전달하는게 아니라, 그 작업의 시점을 실제 기능이 필요한 시점까지 미루고 객체가 메모리에 존재하지 않는 상태에서도 기본적인 정보를 참조, 설정할 수 있도록 하기 위해서 일반적으로 사용된다.
또한, 일종의 인터페이스를 통해 크리티컬한 업무를 직접적으로 조작하지 못하게 하기 위해서도 사용된다.
프록시 패턴은 일반적으로 가상프록시, 원격프록시, 보호프록시로 나뉜다.
각각의 종류에 따라 프록시 패턴을 적용하는 이유와 목적도 달라진다.
해당 객체 또는 기능을 필요로 하는 시점까지 객체 생성을 미루고, 객체가 생성된 것처럼 동작하도록 만들고 싶을 때 사용된다. 프록시 객체에서 기본적인 작업만을 처리하고, 리소스가 많이 필요한 작업을 수행할 때만 주체 객체를 생성해서 처리한다. (Lazy Loading)
원격 객체에 대한 접근을 관리하는 로컬 프록시 객체를 통해 관리하고, 원격 객체의 대리자 역할을 하기 위해 사용한다.
실제 객체에 대한 접근을 제어하고, 객체 별 접근 권한을 분리하고 싶을 때 사용한다. 프록시 객체에서 이러한 접근 제어 부분을 관리한다.
가장 기본적인 가상프록시를 통해 프록시 패턴에 대한 감을 잡아보자.
아래 예시는 대용량 파일을 불러와서 사용해야하는 경우 프록시 패턴을 사용하지 않고 일반적인 구현 방법으로 설계한 케이스다.
import java.io.*;
public class Image {
private String path;
private String fileName;
private InputStream image;
public Image(String path, String fileName) {
this.path = path;
this.fileName = fileName;
load();
}
private void load() {
try {
image = new BufferedInputStream(new FileInputStream(path + fileName));
} catch (IOException e) {
e.printStackTrace();
}
}
public void display() {
System.out.println("path: " + path);
System.out.println("fileName: " + fileName);
// 이미지 내용 출력
}
}
public class Test {
public static void main(String[] args) {
Image image = new Image("C:\\", "image.jpg");
image.display();
}
}
이미지에 대한 텍스트 정보만 확인하고 싶은 경우에도, 먼저 이미지를 읽어온 후에 뿌려줘야 하므로 전체를 불러온 후에야 원하는 작업을 할 수 있다. 하지만 여기서 대용량 이미지라고 가정했으므로, 처음부터 초기화를 실행해주기에는 부담스럽다는 문제점이 발생한다.
프록시 패턴은 공통 메소드를 지니는 interface를 정의하고, 기존 클래스를 실질적인 객체를 가지는 클래스와 프록시 클래스로 분리하여 위 문제를 해결할 수 있다.
public interface Displayable {
void display();
}
public class ProxyImage implements Displayable {
private String fileName;
private String path;
private RealImage image;
public ProxyImage(String path, String fileName) {
this.path = path;
this.fileName = fileName;
}
@Override()
public void display() {
realize();
image.display();
}
private synchronized void realize() {
if (image == null) {
image = new RealImage(path, fileName);
}
}
public String getFileName() {
return fileName;
}
public String getPath() {
return path;
}
}
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class RealImage implements Displayable {
private String path;
private String fileName;
private InputStream image;
public RealImage(String path, String fileName) {
this.path = path;
this.fileName = fileName;
load();
}
private void load() {
try {
image = new BufferedInputStream(new FileInputStream(path + fileName));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void display() {
System.out.println("path: " + path);
System.out.println("fileName: " + fileName);
// 이미지 내용 출력
}
}
프록시 패턴을 사용하기 위해서는 추가적인 클래스와 인터페이스를 정의해줘야하므로 복잡해진다는 문제점과 객체 생성 과정의 상호배제 작업이 필요해서 성능이 저하될 수 있다는 문제가 발생한다.
하지만 실질적인 객체 생성을 필요할 때까지 연기하여 초기화 과정의 오버헤드를 줄일 수 있다는 장점을 지닌다.