객체 접근 제어를 위한 대리자
프록시(Proxy)는 ‘대리자’, ‘대리인’을 의미합니다. 변호사, 부동산 중개인, 대변인 등 실체를 대신하는 형태입니다. 변호사가 없다면 법원에서 당사자가 직접 변호를 해야 합니다. 법적인 지식이 없는 건 둘째 치고, 상대방과 치열한 다툼을 하는 건 여간 힘든 일이 아닙니다. 부동산 중개인이 없으면 직접 집집마다 방문하고, 주인과 스케줄 조정 및 협상을 해야 합니다. 보험도 마찬가지입니다. 사고가 났을 경우 보험회사가 사고 처리를 대신해 줍니다.
또 다른 예로 회사 규모가 큰 기업의 경우 비서를 따로 둡니다. 대표가 일일이 스케줄과 미팅할 사람을 챙기면, 정작 중요한 업무를 할 수 없기에, 비서라는 프록시가 대신합니다.
1980년대 중반 개발된 초기 분산 객체 시스템들(예: Xerox PARC의 Cedar 시스템)에서 이미 프록시 개념이 사용되었고, 분산 시스템, 네트워킹, 운영체제 등 다양한 컴퓨터 과학 분야에서 오랫동안 활용되었습니다. 이를 1994년 GoF에 의해 문서로 프록시(Proxy) 패턴으로 체계화되었습니다.
프록시 패턴은 원격 프록시(Remote Proxy), 가상 프록시(Virtual Proxy), 보호 프록시(Protection Proxy), 캐싱 프록시(Caching Proxy), 스마트 참조 프록시(Smart Reference Proxy)가 있습니다.
원격 프록시는 원격에 있는 객체에 대한 로컬 대리자(마치 대사관처럼) 역할을 하며, 가상 프록시는 필요할 때까지 객체 생성을 지연시키고, 보호 프록시는 접근 권한을 제어하며, 캐싱 프록시는 반복적인 요청의 결과를 캐싱하여 성능 향상, 스마트 참조 프록시는 객체가 참조될 때 추가 작업 수행을 합니다.
가상 프록시를 사용하여 무거운 이미지 로딩을 필요할 때까지 지연시키는 예제를 살펴보겠습니다.
// 이미지 인터페이스
interface Image {
void display();
}
// 실제 이미지 클래스
class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk();
}
private void loadFromDisk() {
System.out.println("디스크에서 로딩 중: " + filename);
// 실제로는 이미지 파일을 읽어오는 무거운 작업이 여기서 수행됨
try {
Thread.sleep(3000); // 무거운 로딩 작업 시뮬레이션
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void display() {
System.out.println("화면에 표시: " + filename);
}
}
// Proxy: 이미지 프록시 클래스
class ProxyImage implements Image {
private String filename;
private RealImage realImage;
public ProxyImage(String filename) {
this.filename = filename;
}
@Override
public void display() {
if (realImage == null) {
System.out.println("프록시: 실제 이미지 객체 생성 시작...");
realImage = new RealImage(filename);
}
realImage.display();
}
}
public class ImageGallery {
public static void main(String[] args) {
// 이미지 배열 생성
Image[] images = new Image[5];
// 프록시 이미지 객체 생성 (아직 실제 이미지는 로드되지 않음)
for (int i = 0; i < 5; i++) {
images[i] = new ProxyImage("Photo_" + (i + 1) + ".jpg");
}
System.out.println("갤러리 애플리케이션 시작");
System.out.println("이미지는 표시할 때만 로드됩니다\n");
// 첫 번째 이미지 표시 (이 시점에 실제 로딩됨)
System.out.println("첫 번째 이미지 선택");
images[0].display();
System.out.println("");
// 같은 이미지 다시 표시 (다시 로드하지 않고 캐시 된 이미지 사용)
System.out.println("첫 번째 이미지 다시 선택");
images[0].display();
System.out.println("");
// 두 번째 이미지 표시
System.out.println("두 번째 이미지 선택");
images[1].display();
}
}
코드를 실행한 결과입니다.
갤러리 애플리케이션 시작
이미지는 표시할 때만 로드됩니다
첫 번째 이미지 선택
프록시: 실제 이미지 객체 생성 시작...
디스크에서 로딩 중: Photo_1.jpg
화면에 표시: Photo_1.jpg
첫 번째 이미지 다시 선택
화면에 표시: Photo_1.jpg
두 번째 이미지 선택
프록시: 실제 이미지 객체 생성 시작...
디스크에서 로딩 중: Photo_2.jpg
화면에 표시: Photo_2.jpg
예제에서 ProxyImage는 RealImage의 로딩을 필요할 때까지 지연시킵니다. 이미지 갤러리를 시작할 때 모든 이미지를 로드하지 않고, 사용자가 특정 이미지를 볼 때만 로드함으로써 초기 로딩 시간을 크게 줄일 수 있습니다.
프록시 패턴은 객체 간 상호작용에 대한 통제력을 높이고자 할 때 유용합니다. 직접 접근하기에는 위험하거나 비용이 많이 드는 리소스를 더 효율적으로 관리할 수 있게 해 줍니다. 이는 마치 현실 세계에서 비서나 법적 대리인이 주인의 시간과 접근성을 관리하는 것과 유사합니다.
인터넷 사용 중에 특정 웹사이트에 접속하려고 할 때, 회사나 학교의 방화벽이 그 접근을 통제하거나 제한한 경험이 있다면 프록시 패턴이 적용된 사례입니다. 또한 대부분의 이미지는 고화질 이미지가 로딩되기 전에 저화질 썸네일이 먼저 표시됩니다. 이 또한 프록시 패턴의 일부입니다.
실체가 크거나, 보안이 필요하거나, 반복되는 작업으로 실체의 부담을 덜기 위해서 등등 프록시 패턴은 이곳저곳에 많이 사용되고 있습니다. ‘대리자’가 없으면 많은 일을 당사자가 감당해야 합니다. ‘대리자’가 있기에 나와 시간의 효율성을 보장합니다.