디자인패턴 (Proxy)

백종현·2023년 6월 24일
0

Proxy 패턴

문제

방대한 양의 시스템 자원을 소비하는 거대한 객체가 있다고 가정하고, 이 객체는 필요할 때가 있기는 하지만, 항상 필요한 것은 아니다. 예를 들어 비대한 사진을 필드로 사용하는 객체가 있는데, 어떤 경우에는 이 필드를 사용하고 어떤 경우에는 사용하지 않는다고 생각해보자. 하지만, 사용을 하지 않는 경우에 이 사진을 계속해서 인스턴스가 만들어질때마다 들어가게 된다면, 엄청난 시간적 + 공간적 낭비가 소요될 것이다.

개념

본인이 아니어도 할 수 있는 일을 맡기고자 대리인을 내세우게 된다. 하지만, 대리인은 결국에 대리를 하는 역할이므로, 할 수 있는 일에는 한계가 생긴다. 따라서 대리인이 할 수 없는 일을 해야 한다면 실제 객체를 불러와 일을 처리하게 된다. 즉, 리소스가 많이 들어 그 일을 하기에 바쁜 본인 객체를 대신해 대리인 객체가 일을 대신해서 처리하는 패턴이다.

코드

이름설명
Printer이름 붙인 프린터를 나타내는 클래스 (본인)
PrintablePrinter와 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()를 통해 실제 객체를 실제로 만들도록 해서 실제 행동을 하도록 하는 것이다.

Proxy 패턴의 요소

Subject(본인) 역할 : Proxy와 RealSubject를 동일 시 하기 위한 인터페이스를 작성하는 부분이다. 이 Subject 덕분에 Client가 Proxy와 RealSubject의 차이를 의식할 필요가 없다. 예제 프로그램에서는 Printable이 이 역할을 한다.
Proxy(대리인) 역할 : Client의 요청을 최대한 처리하고, 만약 본인이 자기 혼자서 처리할 수 없는 경우가 생기면 실제로 RealSubject를 생성하여 처리하도록 하는 것이다. Subject 인터페이스의 구현체이다. 예제 프로그램에서는 PrinterProxy가 이 역할을 맡았다.
RealSubject(실제 본인)의 역할 : 대리인 만으로 감당할 수 없을 때 등장하는 본인인 RealSubject이다. 이 또한 Subject 인터페이스의 구현체이다. 예제 프로그램에서는 Printer 클래스이다.

HTTP 프록시?

CDN을 생각해보자. 실제로 최신 정보가 필요한 경우나, 그 값이 없는 경우에 웹 서버로 가서 웹 페이지를 가지러 간다. 이처럼 웹 페이지 캐싱을 생각했을 때에도 프록시 패턴이 적용된다고 생각할 수 있다. 이 경우 웹 브라우저 Client 역, HTTP 프록시(CDN)가 Proxy 역, 웹 서버가 RealSubject 역을 맡고있다고 생각할 수 있다.

추가

JPA에서 적용된 Lazy Loading이 프록시 패턴을 통해 구현이 되었다. 만약 그 클래스의 필드를 사용하지 않는다면, 대리인을 통해 임시로 객체를 넣어놓고, 실제 필요한 경우에 실제 객체를 가져와 사용할 수 있다. 즉 본인이 아니어도 할 수 있는 일을 대리인을 통해 이 역할을 수행하도록 하는 것이다.

참조 :
Java 언어로 배우는 디자인 패턴 입문

profile
노력하는 사람

0개의 댓글