디자인패턴

[헤드퍼스트 디자인패턴] 프록시 패턴

hongyb 2024. 8. 30. 01:27

프록시 패턴을 알아보자.

프록시 패턴

프록시 패턴

특정 객체로의 접근을 제어하는 대리인(특정 객체를 대변하는 객체)을 제공한다. 프록시 패턴은 실제 객체에 대한 접근을 제어하고, 추가적인 기능(로깅, 캐싱, 접근 제어 등)을 제공할 때 유용하다.

 

프록시 패턴 개념과 목적

프록시 패턴은 실제 객체와 그 객체의 대리자 역할을 하는 객체(프록시 객체) 사이에 인터페이스를 정의하여, 클라이언트가 직접 실제 객체에 접근하지 않고, 프록시 객체를 통해 접근하도록 만든다. 프록시 객체는 클라이언트의 요청을 실제 객체로 전달하고, 그 결과를 클라이언트에게 반환하는 역할을 한다.

 

프록시 패턴의 주요 목적은 다음과 같다.

  1. 접근 제어: 클라이언트가 실제 객체에 접근하기 전에 특정 조건을 확인하거나, 특정 사용자의 접근을 제한할 수 있다.
  2. 지연 초기화(Lazy Initialization): 실제 객체의 생성이 비용이 많이 드는 경우, 필요할 때까지 객체 생성을 지연시킬 수 있다.
  3. 로깅 및 감사(Auditing): 객체에 대한 모든 접근이나 요청을 기록할 수 있다.
  4. 원격 프록시(Remote Proxy): 실제 객체가 다른 주소 공간(예: 원격 서버)에 있는 경우, 클라이언트는 로컬 프록시를 통해 원격 객체에 접근할 수 있다.
  5. 캐싱(Caching): 결과를 캐싱하여 동일한 요청에 대해 빠른 응답을 제공한다.

 

프록시 패턴은 다양한 유형으로 나눌 수 있으며, 각 유형은 특정 목적을 위해 사용된다.

  1. 가상 프록시 (Virtual Proxy):
    • 실제 객체의 생성 비용이 높을 때 사용된다. 프록시가 실제 객체의 생성을 지연시켜, 필요할 때까지 생성하지 않도록 한다.
    • 예: 이미지 뷰어에서 이미지를 표시할 때, 이미지가 실제로 필요한 순간까지 로드를 지연시킨다.
  2. 원격 프록시 (Remote Proxy):
    • 실제 객체가 다른 주소 공간(네트워크 상의 원격 서버)에 있을 때 사용된다.
    • 클라이언트는 로컬 프록시를 통해 원격 객체에 접근하고, 원격 호출을 통해 작업을 수행한다.
    • 예: 분산 객체 시스템에서, 클라이언트는 원격 서버의 객체를 로컬 프록시로 호출할 수 있다.
  3. 보호 프록시 (Protection Proxy):
    • 실제 객체에 대한 접근을 제한할 때 사용된다.
    • 프록시는 특정 사용자나 클라이언트의 권한을 확인하고, 권한이 없는 경우 접근을 거부할 수 있다.
    • 예: 관리자가 아닌 사용자가 특정 파일을 삭제하지 못하도록 보호 프록시를 사용할 수 있다.
  4. 스마트 프록시 (Smart Proxy):
    • 실제 객체에 대한 접근 전에 추가 작업을 수행하는 프록시.
    • 예: 실제 객체의 메서드를 호출하기 전에 로깅, 인증, 캐싱, 원격 호출 처리 등을 수행할 수 있다.

 

프록시 패턴 클래스 다이어그램 및 구현

프록시 패턴 클래스 다이어그램은 다음과 같다.

 

Proxy와 RealSubject 모두 Subject 인터페이스를 구현한다. 이러면 어떤 클라이언트에서든 프록시를 주제와 똑같은 식으로 다룰 수 있다. RealSubject는 진짜 작업을 대부분 처리하는 개게이다.

Proxy는 그 객체로의 접근을 제어하는 객체이다. Proxy에는 진짜 작업을 처리하는 객체의 레퍼런스가 들어있다. 진짜 객체가 필요하면 그 레퍼런스를 사용해 요청을 전달한다.

Proxy에서 RealSubject의 인스턴스를 생성하거나, 그 객체의 생성 과정에 관여하는 경우가 많다.

 

위 클래스 다이어그램을 참고해서 간단한 Proxy 예제를 만들어보겠다. 가상 프록시를 구현한 예제이다.

 

다음과 같이 Subject 인터페이스를 정의한다.

public interface Image {
    void display();
}

 

RealSubject를 정의한다. Subject를 상속받고 오버라이드를 해야 한다. 

public class RealImage implements Image {
    private String fileName;

    public RealImage(String fileName) {
        this.fileName = fileName;
        loadFromDisk(fileName);
    }

    private void loadFromDisk(String fileName) {
        System.out.println("Loading " + fileName);
    }

    @Override
    public void display() {
        System.out.println("Displaying " + fileName);
    }
}

 

Proxy 객체를 정의한다. 마찬가지로 Subject를 상속받아야 하며 RealSubject의 레퍼런스가 들어있다. 클라이언트 요청이 오면 진짜 객체의 레퍼런스를 사용해 요청을 전달한다.

public class ProxyImage implements Image {
    private RealImage realImage;
    private String fileName;

    public ProxyImage(String fileName) {
        this.fileName = fileName;
    }

    @Override
    public void display() {
        if (realImage == null) {
            realImage = new RealImage(fileName); // 실제 객체를 생성 (지연 초기화)
        }
        realImage.display(); // 실제 객체의 메서드 호출
    }
}

 

다음은 클라이언트 코드이다.

public class ProxyPatternExample {
    public static void main(String[] args) {
        Image image = new ProxyImage("test_image.jpg");

        // 이미지가 처음 요청될 때만 실제 객체를 생성하여 로드합니다.
        image.display(); // 로딩 후 표시
        System.out.println("");

        // 이후에는 로드된 이미지를 사용합니다.
        image.display(); // 바로 표시
    }
}

 

동적 프록시

동적 프록시(dynamic proxy)는 자바의 javja.lang.reflect 패키지를 활용한다. 이 패키지를 사용하면 즉석에서 하나 이상의 인터페이스를 구현하고 지정한 클래스에 메소드 호출을 전달하는 프록시 클래스를 만들 수 있다. Java의 리플렉션(Reflection) API를 사용하여 인터페이스를 구현하는 프록시 객체를 생성하며, 개발자는 프록시 객체의 메서드 호출을 처리하는 로직을 정의할 수 있다.

 

동적 프록시 클래스 다이어그램은 다음과 같다.

 

Java에서 동적 프록시는 다음 두 가지 주요 구성 요소를 사용한다

  1. java.lang.reflect.Proxy 클래스:
    • 동적 프록시 객체를 생성하는 데 사용되는 Java 표준 클래스.
    • newProxyInstance() 메서드를 사용하여 런타임에 프록시 객체를 생성한다.
  2. java.lang.reflect.InvocationHandler 인터페이스:
    • 프록시 객체의 메서드 호출을 처리하는 인터페이스이다.
    • invoke() 메서드를 구현하여 프록시 객체의 메서드 호출을 가로채고, 호출 전에/후에 추가 로직을 정의할 수 있다.

자바에서 Proxy 클래스를 생성해 주므로 Proxy 클래스에게 무슨 일을 해야 하는지 알려줘야 한다. 필요한 코드를 프록시 클래스에 넣을 수 없기 때문에 InvocationHandler에 넣어야 한다. InvocationHandler는 프록시에 호출되는 모든 메서드에 응답한다. Proxy에 메서드 호출을 받으면 항상 InvocationHandler에 진짜 작업을 부탁한다고 생각하면 된다. 

 

다음은 간단한 예제이다.

 

Subject 인터페이스를 선언한다.

public interface Subject {
    public void request();
}
 

RealSubject를 구현한다. 

public class RealSubject implements Subject {

    @Override
    public void request() {
        System.out.println("request method called");
    }
}

 

InvocationHandler를 구현한다. InvocationHandler는 Java 내부에 선언되어 있기 때문에 따로 만들 필요 없다. InvocationHandler는 invoke(Object proxy, Method method, Object[] args) 메서드만 갖고 있다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class InvocationHandlerImpl implements InvocationHandler {
    private Object target;

    public InvocationHandlerImpl(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("메서드 호출 전: " + method.getName()); // 메서드 호출 전 로그
        Object result = method.invoke(target, args); // 실제 객체의 메서드 호출
        System.out.println("메서드 호출 후: " + method.getName()); // 메서드 호출 후 로그
        return result;
    }
}

 

테스트 코드와 결과는 다음과 같다.

import java.lang.reflect.Proxy;

public class ProxyTest {
    public static void main(String[] args) {
        Subject subject = new RealSubject();

        Subject proxySubject = (Subject) Proxy.newProxyInstance(
                subject.getClass().getClassLoader(),
                subject.getClass().getInterfaces(),
                new InvocationHandlerImpl(subject)
        );

        proxySubject.request();
    }
}