[DesignPattern] 프록시 패턴

프록시 패턴


proxy는 대리인이라는 의미이다.

대리인이란 일을 해야 할 본인을 대신(대리)하는 사람이다. 본인이 아니라도 가능한 일을 맡기기 위해서 대리인을 세운다.

대리인은 어디까지나 대리에 지나지 않기 때문에 할 수 있는 일에는 한계가 있다.

대리인이 할 수 있는 범위를 넘는 일이 발생하면, 대리인은 본인한테 와서 상담을 한다.

오브젝트(객체) 지향에서는 '본인'도 '대리인'도 오브젝트(객체)가 된다. 바빠서 일을 할 수 없는 오브젝트 대신에 대리인 오브젝트가 어느 정도 일을 처리하게 된다.




▶ 등장인물

 

 

 Printer

 이름있는 프린터를 나타내는 클래스(본인)

 Printable

 Printer와 PrinterProxy 공통의 인스턴스

 PrinterProxy

 이름있는 프린터를 나타내는 클래스(대리인)

 Main

 동작 테스트용 클래스


▶ 예제 프로그램 해설

아래는 Proxy 패턴을 사용한 예제이다.

이번 예제 프로그램은 '이름이 있는 프린터'이다. 프린터라고 해도 실제로는 화면에 문자열을 표시할 뿐이다. Main 클래스는 PrinterProxy 클래스의 인스턴스(대리인)를 생성한다.

그 인스턴스에 'Alice'라는 이름을 붙이고 그 이름을 표시한다. 그 후 'Bob'이라는 이름으로 변경해서 그 이름을 표시한다. 이름의 설정과 취득에서는 아직 실제 Printer 클래스의 인스턴스(본인)는 생성되지 않는다. 이름의 설정과 취득 부분은 PrinterProxy 클래스가 대리로 실행한다. 마지막에 print 메소드를 호출해서 실제로 프린터를 실행하는 단계가 되어서야 비로소 Printer 클래스는 Printer 클래스의 인스턴스를 생성한다. 그리고 PrinterProxy 클래스와 Printer 클래스를 동일시 하기 위해 Printable 인터페이스가 정의되어 있다.

여기에서는 Printer 클래스의 인스턴스 생성에 많은 시간이 걸린다는 것을 전제로 프로그램을 만든다. 시간이 걸린다는 것을 표현하기 위해서 생성자로부터 heavyJob 메서드를 호출해서 일부러 '무거운일'을 실행시킨다.


1. Printer 클래스

- RealSubject(실제의 주체)의 역할: '대리인'인 Proxy 역할에서 감당할 수 없는 일이 발생했을 때 등장하는 것이 '본인'인 RealSubject 역할이다. 이 역할도 Proxy 역할과 마찬가지로 Subject 역할에서 정해져 있는 인터페이스(API)를 구현한다.

public class Printer implements Printable {

    private String name;

    public Printer() {

        heavyJob("Printer의 인스턴스를 생성 중");

    }

    public Printer(String name) { // 생성자

        this.name = name;

        heavyJob("Printer의 인스턴스 (" + name + ")을 생성 중");

    }

    public void setPrinterName(String name) { // 이름의 설정

        this.name = name;

    }

    public String getPrinterName() { // 이름의 취득

        return name;

    }

    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("완료");

    }

}

Printer 클래스는 '본인'을 표시하는 클래스이다. 생성자에서는 앞서 말했듯이 더미의 '무거운 일' 즉, heavyJob을 실행하고 있다. setPrinterName은 이름을 설정하는 메서드이고, getPrinterName은 이름을 취득하는 메서드이다. print 메서드는 프린터의 이름을 붙여서 문자열을 표시하고 있다. heavyJob 메서드는 실행에 5초가 걸리는 무거운 일을 표현하고 있다.


2. Printable 인터페이스

- Subject(주체)의 역할: Proxy 역할과 RealSubject 역할을 동일시 하기 위한 인터페이스(API)를 결정한다.

Subject 역할이 있는 덕분에 Client 역할은 Proxy 역할과 RealSubject 역할의 차이를 의식할 필요가 없다. 

public interface Printable {

    public abstract void setPrinterName(String name); // 이름의 설정

    public abstract String getPrinterName(); // 이름의 취득

    public abstract void print(String string); // 문자열 표시(프린트 아웃)

}

Printable 인터페이스는 PrinterProxy 클래스와 Printer 클래스를 동일시 하기 위한 것이다. setPrinterName 메서드는 이름의 설정, getPrinterName 메서드는 이름의 취득, 그리고 print 메서드는 프린트 아웃(문자열 표시)을 위한 것이다.


3. PrinterProxy 클래스

- Proxy(대리인)의 역할: Proxy의 역할은 Client 역할의 요구를 할 수 있는 만큼 처리를 한다. 만약, 자신만으로 처리할 수 없으면 Proxy 역할은 RealSubject 역할에게 처리를 맡긴다. Proxy 역할은 정말로 RealSubject 역할이 필요해지면 그때 RealSubject 역할을 생성한다. Proxy 역할은 Subject 역할에서 정해지는 인터페이스(API)를 구현한다.

public class PrinterProxy implements Printable {

    private String name; // 이름

    private Printer real; // 「본인」

    public PrinterProxy() {

    }

    public PrinterProxy(String name) { // 생성자

        this.name = name;

    }

    public synchronized void setPrinterName(String name) {  // 이름의 설정

        if (real != null) {

            real.setPrinterName(name);  // 「본인」에게도 설정한다

        }

        this.name = name;

    }

    public String getPrinterName() {  // 이름의 설정

        return name;

    }

    public void print(String string) { // 표시

        realize();

        real.print(string);

    }

    private synchronized void realize() { // 「본인」을 생성

        if (real == null) {            

            real = new Printer(name);

        }                           

    }

}

Proxy 패턴의 중심은 PrinterProxy 클래스이다. 

PrinterProxy 클래스는 대리인의 역할을 수행하며, Printable 인터페이스를 구현한다. name 필드는 이름을 저장하고, real 필드는 '본인'을 저장한다.

생성자는 이름을 설정한다.(이 시점에서 '본인'은 만들어지지 않는다.)

setPrinterName 메서드는 새로운 이름을 설정한다. 만약,real이 null이 아니면(즉, '본인'이 이미 만들어져 있으면), 본인에 대해서도 그 이름을 설정한다.

그러나 real이 null이면(즉, '본인'이 아직 만들어져 있지 않으면), 자신(PrinterProxy의 인스턴스)의 name 필드에만 이름을 설정한다.

getPrinterName 메서드는 자신의 name 필드의 값을 반환할 뿐이다.

print 메서드는 대리인이 가능한 일의 범위를 넘어서기 때문에 여기에서 realize 메서드를 호출해서 본인을 생성한다. realize는 '현실화하다'라는 의미이다.

realize 메서드를 실행한 후 real 필드에는 본인(Printer 클래스의 인스턴스)이 저장되어 있기 때문에 real.print를 호출한다. 이것은 위임이다.

setPrinterName과 getPrinterName을 여러 차례 호출해도, Printer의 인스턴스는 생성되지 않는다.

Printer의 인스턴스가 생성되는 것은 '본인'이 정말로 필요할 때이다.(본인이 생성되었는지 아닌지를 PrinterProxy의 이용자는 전혀 알 수 없고, 알 필요도 없다.)

realize 메서드는 단순하다. real 필드가 null이면 new Printer에 의해 Printer의 인스턴스를 만든다. 그리고 real 필드가 null이 아니면 (즉, 이미 만들어져 있으면) 아무 처리도 하지 않는다.

기억해야 할 점은 Printer 클래스는 PrinterProxy의 존재를 모른다는 점이다. 자신이 PrinterProxy을 경유해서 호출되고 있는지 아니면 직접 호출되고 있는지 Printer 클래스는 모른다.

반면에 PrinterProxy 클래스는 Printer 클래스를 알고 있다. 왜냐하면 PrinterProxy 클래스의 real 필드는 Printer형이고, PrinterProxy 클래스의 소스 코드안에는 Printer 클래스 이름이 기술되어 있기 때문이다. 이처럼 PrinterProxy 클래스는 Printer 클래스와 깊이 관련되 부품이다.


cf.) PrinterProxy 클래스에서 setPrinterName 메서드와 realize 메서드가 synchronized 메서드로 되어 있는 이유

synchronized 메서드로 하지 않은 경우, 복수의 스레드로부터 setPrintName과 realize가 개별적으로 호출되면, PrinterProxy 클래스의 name과 Printer 클래스의 name에 차이가 생길 경우가 있다.

최초에 PrinterProxy의 name 필드의 값이 'Alice'이고, real 필드의 값이 null(즉, Printer 클래스의 인스턴스는 아직 생성되어 있지 않다.)이라고 가정하자.

스레드 A가 setPrinterName("Bob")을 실행함과 동시에 스레드 B가(print 메서드 경유로) realize 메서드를 호출했다고 하자. 만약 스레드가 교체되면 PrinterProxy 클래스의 name 필드의 값은 "Bob"이 되지만, Printer의 name 필드의 값은 "Alice"로 되고 만다.

setPrintName 메서드와 realize 메서드를 synchronized 메서드로 하면 이와 같은 스레드 교체가 발생하지 않는다. synchronized 메서드에 의해 real 필드의 값에 대한 판단과 값의 변경이 제각각 실행되지 않도록 하고 있다. synchronized 메서드를 이용해서 real 필드를 지키고 있는 것이 된다.


4. Main 클래스

- Client(의뢰인)의 역할: Proxy 패턴을 이용하는 역할이다.

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.");

    }

}

Main 클래스는 PrinterProxy를 경유해서 Printer를 이용하는 클래스이다. 이 클래스는 처음에 PrinterProxy를 생성하고, getPrinterName을 이용해서 이름을 표시한다.

그리고 나서 setPrinterName으로 이름을 설정하고, 마지막에 print로 "Hello, world"라고 표시한다.

실행결과를 보고 이름의 설정과 표시를 하는 동안에는 Printer의 인스턴가 생성되지 않고, print 메서드를 호출한 후에 생성되고 있는 점을 확인하자.

실행결과)

이름은 현재 Alice입니다.

이름은 현재 Bob입니다.

Printer의 인스턴스 (Bob)을 생성 중.....완료

=== Bob ===

Hello, world.