Featured image of post 11. 객체 접근 제어하기 - 프록시 패턴

11. 객체 접근 제어하기 - 프록시 패턴

헤드 퍼스트 디자인 패턴

프록시 패턴은 특정 객체로의 접근을 제어하는 대리인(특정 객체를 대변하는 객체)를 제공한다.

프록시 패턴이란?

프록시 패턴은 한 객체가 다른 객체에 대한 인터페이스 역할을 수행하여 접근을 제어하거나 대리 역할을 수행하는 디자인 패턴이다.

접근을 제어하는 방법을 달리하여 여러 변형이 존재한다.

  • 원격 프록시(Remote Proxy)
    • 원격 객체에 대한 로컬 표현을 제공하여, 원격 서버에 있는 객체에 쉽게 접근할 수 있도록 한다.
  • 가상 프록시(Virtual Proxy)
    • 객체의 생성 및 초기화에 비용이 많이 들 때, 객체가 실제로 필요한 시점까지 생성을 지연시키는 가상 객체를 제공한다.
  • 보호 프록시(Protection Proxy)
    • 객체에 대한 접근을 제어하여, 특정 사용자나 클라이언트에 대한 권한 검사를 수행한다.
  • 캐싱 프록시(Caching Proxy)
    • 이전에 수행한 연산 결과를 캐시하여 동일한 요청에 대한 성능을 향상시킨다.

프록시 패턴을 사용하면 원격 객체, 생성하기 힘든 객체, 보안이 중요한 객체와 같은 다른 객체로의 접근을 제어하는 대리인 객체를 만들 수 있다.

이를 통해 복잡한 시스템에서 객체 간의 통신과 상호작용을 조절하고 향상시키는 데 유용하게 사용될 수 있다.

구조

classDiagram
    class Subject {
        << interface >>
        request()*
    }
    
    class RealSubject {
        request()
    }
    
    class Proxy {
        request()
    }
    
    Subject <|.. RealSubject
    Subject <|.. Proxy
    Proxy --> RealSubject : subject
  • Subject(주체)
    • 실제 서비스를 제공하는 객체를 나타내는 인터페이스를 정의한다.
  • RealSubject(실제 주체)
    • Subject의 실제 구현을 제공하는 클래스
    • 이 객체에 대한 접근을 제어하거나 비용이 많이 드는 작업을 수행한다.
  • Proxy(프록시)
    • Subject와 동일한 인터페이스를 구현하며, RealSubject에 대한 참조를 가지고 있다.
    • 실제 서비스 객체에 대한 접근을 제어하거나 추가적인 기능을 제공한다.

RealSubjectProxySubject 인터페이스를 구현하므로 RealSubject가 들어가야 할 자리에 Proxy를 대신 넣을 수 있다.

진짜 작업은 RealSubject 객체가 처리하며, Proxy는 이 객체의 대변인 역할을 하면서 RealSubject로의 접근을 제어하게된다.

ProxyRealSubject의 레퍼런스를 포함하며(직법 생성하거나 제거하기도 함) 클라이언트는 항상 Proxy를 통해 데이터를 주고받게 된다.

원격 프록시

원격 프록시는 대리인의 어떤 메소드를 호출하면 원격 객체에게 해당 메소드 호출을 전달하는 원격 객체의 로컬 대리인 역할을 하게된다.

클라이언트 객체는 원격 객체의 메소드 호출을 하는 것처럼 행동하지만 실제로는 로컬 힙에 들어있는 프록시 객체의 메소드를 호출하며, 프록시에서 네트워크 통신과 관련된 저수준 작업을 처래히준다.

원격 프록시(Remote Proxy)는 클라이언트와 서버 사이에서 객체에 대한 원격 액세스를 제공하며, 객체의 메서드 호출을 원격으로 전달하여 실행하는 데 중접을 둔다.

flowchart LR
    subgraph 원격
      a((클라이언트)) -- "request()" -->
      b((프록시))
    end
    
    subgraph 실제
      c((RealSubject))
    end
    
    b -. "request()" .-> c
  1. 원격 객체 엑세스
    • 클라이언트가 로컬이 아닌 다른 컴퓨터에 위치한 객체에 액세스해야 할 때, 원격 프록시를 사용하여 해당 객체에 접근한다.
  2. 보안 및 권한 제어
    • 원격 프록시를 사용하여 원격 객체에 대한 접근을 제어하고, 보안 및 권한 검사를 수행할 수 있다.
  3. 프록시 패턴의 응용
    • 일반적인 프록시 패턴에서 원격 프록시를 사용하여 클라이언트와 실제 객체 사이의 중각 매개체로서 동작하게 할 수 있다.

Java RMI(Remote Method Invocation)

원격 프록시는 JavaRMI와 같은 기술을 사용하여 구현될 수 있다.

클라이언트는 로컬에서 마치 객체를 다루는 것처럼 원격 프록시를 사용하여 원격 객체의 메서드를 호출할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 예시: 원격 프록시 인터페이스
import java.rmi.Remote;
import java.rmi.RemoteException;

// 표식용(Maker) Remote 인터페이스를 확장하여 인터페이스가 원격 호출을 지원한다는 사실을 알린다.
public interface MyRemoteInterface extends Remote {
    // 모든 원격 메소드 호출은 위험이 따르며 모든 메소드에 RemoteException을 선언하여 클라이언트에서 예외를 처리할 수 있도록 한다.
    void myRemoteMethod() throws RemoteException;
    
    // 원격 메소드의 인자와 리턴값은 반드시 원시 형식(primitive) 또는 Serializable 형식으로 선언해야한다.
    String sayHello() throws RemoteException;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 예시: 원격 프록시 구현
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class MyRemoteObject extends UnicastRemoteObject implements MyRemoteInterface {
    protected MyRemoteObject() throws RemoteException {
        super();
    }

    @Override
    public void myRemoteMethod() throws RemoteException {
        // 실제 원격 메서드 구현
        System.out.println("Executing remote method");
    }
}

가상 프록시

가상 프록시는 프록시 패턴의 한 종류로, 객체의 생성 및 초기화에 비용이 많이 들 때, 실제로 객체가 필요한 시점까지 객체를 생성하지 않고 대리 객체를 사용하는 패턴이다.

이를 통해 성능 향상 및 자원 절약이 가능하며, 사용자는 가상 프록시를 통해 실제 객체에 접근할 수 있다.

flowchart LR
    a((클라이언트)) -- "request()" --> 
    b((프록시)) -->
    c((RealSubject))
  1. 비용이 많이 드는 객체 생성
    • 객체의 생성이나 초기화에 많은 비용이 들 때, 해당 객체를 실제로 사용하기 직전까지 객체를 생성하지 않고 대리 객체를 사용하여 필요한 경우에만 생성한다.
  2. 느린 초기화 작업
    • 객체의 초기화 작업이 느리게 수행되는 경우, 초기화 작업이 완료된 후에만 실제 객체를 생성하도록 하는 가상 프록시를 사용하여 지연 로딩을 구현할 수 있다.
  3. 자원 소모 최적화
    • 자원을 효율적으로 관리하기 위해 필요할 때만 실제 객체를 생성하고 사용하는 경우, 가상 프록시를 적용하여 자원 소모를 최적화 할 수 있다.

가상 프록시는 실제 객체를 나타내는 인터페이스를 정의하고, 이 인터페이스를 구현하는 가상 프록시 클래스를 만들어 사용하게 된다.

이를 통해 실제 객체의 생성 및 초기화를 지연시킨다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 예시: 가상 프록시 인터페이스
public interface RealObject {
    void performOperation();
}

// 예시: 실제 객체 구현
public class RealObjectImpl implements RealObject {
    public RealObjectImpl() {
        // 객체 생성 및 초기화 작업
    }

    @Override
    public void performOperation() {
        // 실제 객체의 동작
        System.out.println("Performing operation");
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 예시: 가상 프록시 구현
public class VirtualProxy implements RealObject {
    private RealObjectImpl realObject;

    @Override
    public void performOperation() {
        // 필요할 때만 실제 객체 생성
        if (realObject == null) {
            realObject = new RealObjectImpl();
        }

        // 가상 프록시를 통해 실제 객체의 동작 호출
        realObject.performOperation();
    }
}

원격 프록시와 가상 프록시의 차이

원격 프록시와 가상 프록시는 둘 다 프록시 패턴의 변형으로, 객체 간의 통신을 지원하거나 객체의 생성 및 초기화를 최적화하기 위해 상용된다.

그러나 각각의 주된 목적과 사용 시나리오가 다르기 때문에 차이가 있다.

  1. 목적
    • 원격 프록시:
      • 분산 시스템에서 객체 간의 통신을 가능케 하는 것이 목적이다.
      • 클라이언트와 서버 간의 통신을 원활하게 하기 위해 객체를 원격으로 호출하고 사용한다.
    • 가상 프록시:
      • 객체의 생성 및 초기화에 대한 비용을 최적화한다.
      • 필요한 시점까지 실제 객체를 생성하지 않고 가상적인 대리 객체를 사용함으로 성능을 향상시키는 것이 목적이다.
  2. 활용
    • 원격 프록시
      • 객체가 서로 다른 주소 공간에 위치해 있을 때 사용된다.
      • 객체의 메서드를 원격으로 호출하여 분산 시스템에서 투명한 원격 접근을 제공한다.
    • 가상 프록시
      • 객체의 생성이나 초기화에 비용이 많이 들거나, 초기화를 지연시켜야 할 때 사용된다.
      • 필요한 시점에만 실제 객체를 상성하여 자원을 효율적으로 활용할 수 있다.
  3. 구현 방식
    • 원격 프록시
      • 원격 객체 간의 통신을 위해 원격 메서드 호출을 지원하는 기술을 활용한다.
      • ex) Java RMI
    • 가상 프록시
      • 객체의 생성 및 초기화를 지연시키는 방식으로 구현된다.
      • 필요할 때만 실제 객체를 생성하고 사용한다.
  4. 주요 특징
    • 원격 프록시
      • 분산 환경에서의 통신에 중점을 두고, 보안, 효율성 투명성 등을 고려해야한다.
    • 가상 프록시
      • 객체의 생성 및 초기화 비용을 최적화한다.
      • 필요한 시점까지 실제 객체를 생성하지 않고 가상적인 대리 객체를 사용하여 성능을 개선한다.

원격 프록시는 주로 분산 시스템에서의 객체 간 통신을 위해 사용되고, 가상 프록시는 객체의 생성과 초기화에 대한 비용을 최적화하기 위해 사용한다.

보호 프록시

보호 프록시는 주로 객체에 대한 접근을 제어하고 보호하는 데 사용된다.

이 패턴은 객체에 대한 클라이언트의 엑세스를 제한하거나, 특정한 권한 검사를 수행하여 보안을 강화할 수 있다.

  1. 접근 제어
    • 특정 객체에 대한 접근을 제한하거나 허용하기 위해 클라이언트의 요청을 검사하고 필요한 권한 검사를 수행한다.
    • ex) 파일, 데이터베이스 등
  2. 보안 강화
    • 객체에 직접적으로 접근하는 것을 방지하여 보안을 강화한다.
    • 실제 객체에 직접 접근하는 대신, 보호 프록시를 통해 간접적으로만 객체에 접근할 수 있다.
  3. 캐싱 및 성능 최적화
    • 객체에 대한 엑세스를 제어하는 동시에, 캐싱과 같은 성능 최적화 기법을 적용하여 불필요한 작업을 줄일 수 있다.
classDiagram
    class Subject {
        << interface >>
        *request()
    }
    
    class RealSubject {
        request()
    }
    
    class Proxy {
        request()
    }
    
    class InvocationHandler {
        << interface >>
        *invoke()
    }
    
    class ConcreteInvocationHandler {
        invoke()
    }

    Subject <|-- RealSubject
    Subject <|-- Proxy
    
    InvocationHandler <|-- ConcreteInvocationHandler

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 보호 프록시 인터페이스
public interface Subject {
    void request();
}

// 실제 객체
public class RealSubject implements Subject {
    @Override
    public void request() {
        System.out.println("RealSubject: Handling request.");
    }
}

// 보호 프록시
public class Proxy implements Subject {
    private RealSubject realSubject;
    private String accessCode; // 예시로 권한 코드를 사용

    public Proxy(String accessCode) {
        this.accessCode = accessCode;
    }

    @Override
    public void request() {
        if (authenticate()) {
            if (realSubject == null) {
                realSubject = new RealSubject();
            }
            realSubject.request();
        } else {
            System.out.println("Access denied. Authentication failed.");
        }
    }

    private boolean authenticate() {
        // 권한 검사 로직
        return "1234".equals(accessCode);
    }
}

정리

  • 프록시 패턴
    • 특정 객체로의 접근을 제어하는 대리인을 제공한다.

  • 프록시 패턴을 사용하면 어떤 객체의 대리인을 내세워서 클라이언트의 접근을 제어할 수 있다.
  • 원격 프록시는 클라이언트와 원격 객체 사이의 데이터 전달을 관리해준다.
  • 가상 프록시는 인스턴스를 만드는 데 많은 비용이 드는 객체로의 접근을 제어한다.
  • 보호 프록시는 호출하는 족의 권한에 따라서 객체에 있는 메소드로의 접근을 제어한다.
  • 그 외에도 캐싱 프록시, 동기화 프록시, 방화벽 프록시, 지연 복사 프록시와 같이 다양한 변형된 프록시 패턴이 있다.
  • 프록시 패턴의 구조는 데코레이터 패턴의 구조와 비슷하지만 그 용도는 다르다.
  • 데코레이터 패턴은 객체에 행동을 추가하지만 프록시 패턴은 접근을 제어한다.
  • 자바에 내장된 프록시 지원 기능을 사용하면 동적 프록시 클래스를 만들어서 원하는 핸들러에서 호출을 처리하도록 할 수 있다.
  • 다른 래퍼를 쓸 때와 마찬가지로 프록시를 쓰면 디자인에 포함되는 클래스와 객체의 수가 늘어난다.