방대한 양의 시스템 자원을 소비하는 거대한 객체가 있다고 가정하고, 이 객체는 필요할 때가 있기는 하지만, 항상 필요한 것은 아니다. 예를 들어 비대한 사진을 필드로 사용하는 객체가 있는데, 어떤 경우에는 이 필드를 사용하고 어떤 경우에는 사용하지 않는다고 생각해보자. 하지만, 사용을 하지 않는 경우에 이 사진을 계속해서 인스턴스가 만들어질때마다 들어가게 된다면, 엄청난 시간적 + 공간적 낭비가 소요될 것이다.
본인이 아니어도 할 수 있는 일을 맡기고자 대리인을 내세우게 된다. 하지만, 대리인은 결국에 대리를 하는 역할이므로, 할 수 있는 일에는 한계가 생긴다. 따라서 대리인이 할 수 없는 일을 해야 한다면 실제 객체를 불러와 일을 처리하게 된다. 즉, 리소스가 많이 들어 그 일을 하기에 바쁜 본인 객체를 대신해 대리인 객체가 일을 대신해서 처리하는 패턴이다.
이름 | 설명 |
---|---|
Printer | 이름 붙인 프린터를 나타내는 클래스 (본인) |
Printable | Printer와 PrinterProxy의 공통 인터페이스 |
PrinterProxy | 이름 붙인 프린터를 나타내는 클래스 (대리인) |
Main | 동작 테스트 용 클래스 |
public class Printer implements Printable {
private String name; // 이름
// 생성자
public Printer() {
heavyJob("Printer 인스턴스 생성 중");
}
// 생성자(이름 지정)
public Printer(String name) {
this.name = name;
heavyJob("Printer 인스턴스(" + name + ") 생성 중");
}
// 이름 설정
@Override
public void setPrinterName(String name) {
this.name = name;
}
// 이름 취득
@Override
public String getPrinterName() {
return name;
}
// 이름 붙여서 표시
@Override
public void print(String string) {
System.out.println("=== " + name + " ===");
System.out.println(string);
}
// 무거운 작업이라고 가정
private void heavyJob(String msg) {
System.out.print(msg);
for (int i = 0; i < 5; i++) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.print(".");
}
System.out.println("완료");
}
}
public interface Printable {
public abstract void setPrinterName(String name); // 이름 설정
public abstract String getPrinterName(); // 이름 획득
public abstract void print(String string); // 문자열 표시(프린트 아웃)
}
public class PrinterProxy implements Printable {
private String name; // 이름
private Printer real; // '본인'
// 생성자
public PrinterProxy() {
this.name = "No Name";
this.real = null;
}
// 생성자(이름 지정)
public PrinterProxy(String name) {
this.name = name;
this.real = null;
}
// 이름 설정
@Override
public synchronized void setPrinterName(String name) {
if (real != null) {
// '본인'에게도 설정한다
real.setPrinterName(name);
}
this.name = name;
}
// 이름 취득
@Override
public String getPrinterName() {
return name;
}
// 표시
@Override
public void print(String string) {
realize();
real.print(string);
}
// 본인 생성
private synchronized void realize() {
if (real == null) {
real = new Printer(name);
}
}
}
public class Main {
public static void main(String[] args) {
Printable p = new PrinterProxy("Alice");
System.out.println("이름은 현재 " + p.getPrinterName() + "입니다.");
p.setPrinterName("Bob");
System.out.println("이름은 현재 " + p.getPrinterName() + "입니다.");
p.print("Hello, world.");
}
}
/*
결과 :
이름은 현재 Alice입니다.
이름은 현재 Bob입니다.
Printer 인스턴스(Bob) 생성 중.....완료 # 추가 설명 : 5초후에 생성이 된다!!
=== Bob ===
Hello, world.
Process finished with exit code 0
*/
실제로 객체가 완전하게 만들어지지 않은 상황이더라도, 즉 대리인을 통해 처리하는 경우 p.getPrinterName()와 p.setPrinterName("Bob");와 같이 PrinterName 변경하는 메소드는 문제가 생기지 않고 대리인이 잘 처리하게 될 것이다.
하지만, p.print()를 사용하는 경우는 (실제로 무거운 작업이라고 생각했을때), 실제 객체가 필요할 것이고, 이는 realize()를 통해 실제 객체를 실제로 만들도록 해서 실제 행동을 하도록 하는 것이다.
Subject(본인) 역할 : Proxy와 RealSubject를 동일 시 하기 위한 인터페이스를 작성하는 부분이다. 이 Subject 덕분에 Client가 Proxy와 RealSubject의 차이를 의식할 필요가 없다. 예제 프로그램에서는 Printable이 이 역할을 한다.
Proxy(대리인) 역할 : Client의 요청을 최대한 처리하고, 만약 본인이 자기 혼자서 처리할 수 없는 경우가 생기면 실제로 RealSubject를 생성하여 처리하도록 하는 것이다. Subject 인터페이스의 구현체이다. 예제 프로그램에서는 PrinterProxy가 이 역할을 맡았다.
RealSubject(실제 본인)의 역할 : 대리인 만으로 감당할 수 없을 때 등장하는 본인인 RealSubject이다. 이 또한 Subject 인터페이스의 구현체이다. 예제 프로그램에서는 Printer 클래스이다.
CDN을 생각해보자. 실제로 최신 정보가 필요한 경우나, 그 값이 없는 경우에 웹 서버로 가서 웹 페이지를 가지러 간다. 이처럼 웹 페이지 캐싱을 생각했을 때에도 프록시 패턴이 적용된다고 생각할 수 있다. 이 경우 웹 브라우저 Client 역, HTTP 프록시(CDN)가 Proxy 역, 웹 서버가 RealSubject 역을 맡고있다고 생각할 수 있다.
JPA에서 적용된 Lazy Loading이 프록시 패턴을 통해 구현이 되었다. 만약 그 클래스의 필드를 사용하지 않는다면, 대리인을 통해 임시로 객체를 넣어놓고, 실제 필요한 경우에 실제 객체를 가져와 사용할 수 있다. 즉 본인이 아니어도 할 수 있는 일을 대리인을 통해 이 역할을 수행하도록 하는 것이다.
참조 :
Java 언어로 배우는 디자인 패턴 입문