디자인패턴

프록시패턴 (Proxy Pattern)

traveler_JH 2024. 12. 15. 23:43

프록시패턴 (Proxy Pattern)

    • 객체에 대한 접근을 제어하거나 추가적인 기능을 제공하기 위해 객체의 대리인 역할을 수행하는 클래스를 만드는 구조적 디자인 패턴
    • 프록시 객체는 실제 객체(Real Subject)에 대한 참조를 유지하며, 클라이언트가 실제 객체에 직접 접근하지 않고 프록시를 통해 간접적으로 접근하도록 만든다.

프록시 패턴의 핵심

  • 접근 제어
    • 권한, 인증, 요청 제한 등을 통해 객체에 대한 접근을 관리한다.
    • 예: 특정 사용자만 데이터베이스 수정 권한을 갖는 경우.
  • 리소스 관리
    • 리소스가 무겁거나 생성 비용이 높은 객체의 생성을 지연하거나 최적화 한다.
    • 예: 대용량 데이터를 지연 로딩(Lazy Loading)으로 처리.
  • 추가 기능
    • 로깅, 캐싱, 요청 검증 등 실제 객체와 독립적으로 부가적인 동작을 수행한다.
    • 예: 객체에 대한 호출 시 로깅 정보를 기록.

구성요소

  • Subject (인터페이스 또는 추상 클래스)
    • 실제 객체(Real Subject)와 프록시(Proxy)가 구현해야 할 공통 인터페이스 또는 추상 클래스
  • Real Subject (실제 객체)
    • 실제 작업을 수행하는 객체
  • Proxy (프록시 객체)
    • Real Subject와 동일한 인터페이스를 구현하며, 클라이언트가 요청을 프록시 객체를 통해 처리하도록 한다
    • 필요에 따라 요청을 가로채거나, 접근을 제어하거나, 추가 로직을 수행한 후 Real Subject로 요청을 전달

프록시 패턴의 유형

  1. 가상 프록시 (Virtual Proxy)
    • 리소스가 무겁거나 비용이 많이 드는 객체를 실제로 생성하기 전에 대체 역할을 수행
    • 예: 대용량 이미지 로딩을 위한 지연 초기화(lazy initialization).
  2. 원격 프록시 (Remote Proxy)
    • 네트워크를 통해 다른 주소 공간에 있는 객체를 대리한다.
    • 예: 원격 서버와의 통신을 단순화하는 Stub.
  3. 보호 프록시 (Protection Proxy)
    • 객체에 대한 접근 제어를 제공합니다. 사용자 권한에 따라 객체 접근을 제한
    • 예: 관리자만 데이터 수정이 가능한 시스템.
  4. 캐싱 프록시 (Caching Proxy)
    • 결과를 캐싱하여 동일한 요청에 대해 성능을 향상
    • 예: 데이터베이스 쿼리 캐싱.
  5. 스마트 프록시 (Smart Proxy)
    • 추가적인 작업(예: 참조 카운팅, 로깅 등)을 수행
    • 예: 객체의 메모리 사용 추적.

예제

from abc import ABC, abstractmethod

# Subject (인터페이스)
class Image(ABC):
    @abstractmethod
    def display(self):
        pass

# Real Subject (실제 객체)
class RealImage(Image):
    def __init__(self, filename):
        self.filename = filename
        self.load_from_disk()

    def load_from_disk(self):
        print(f"Loading {self.filename} from disk...")

    def display(self):
        print(f"Displaying {self.filename}")

# Proxy (프록시 객체)
class ProxyImage(Image):
    def __init__(self, filename):
        self.filename = filename
        self.real_image = None

    def display(self):
        if not self.real_image:  # RealImage 객체가 없으면 생성
            self.real_image = RealImage(self.filename)  # 지연 초기화
        self.real_image.display()  # RealImage의 display 메서드 호출

# 클라이언트 코드
if __name__ == "__main__":
    image = ProxyImage("example.jpg")
    
    # 첫 번째 호출에서 실제 객체 생성 및 표시
    image.display()
    
    # 두 번째 호출에서는 이미 생성된 객체를 재사용
    image.display()
 

작동 방식

    1. 클라이언트가 ProxyImage 객체를 생성
      • ProxyImage 클래스의 __init__ 메서드가 호출
      • filename을 저장하고 real_image를 None으로 초기화.
      • 실제 이미지(RealImage)는 아직 생성되지 않음
      • 목적: 리소스 낭비를 줄이기 위해 실제 객체 생성 시점을 지연.
    2. display() 메서드 첫 번째 호출
      • 프록시 객체(ProxyImage)의 display 메서드가 호출
      • real_image가 None인지 확인
      • 조건이 참이므로 RealImage 객체를 생성
      • RealImage 생성자(__init__)가 호출되어 디스크에서 이미지를 로드
        • print : Loading example.jpg from disk...
      • 이후, 생성된 RealImage 객체의 display 메서드를 호출
        • print : Displaying example.jpg
    3. display() 메서드 두 번째 호출
      • 프록시 객체의 display 메서드가 호출
      • real_image가 이미 생성되어 있으므로 조건이 거짓
      • 이미 생성된 RealImage 객체의 display 메서드만 호출
        • print : Displaying example.jpg

핵심:

  • 지연 초기화(Lazy Initialization)를 통해 불필요한 리소스 낭비를 방지.
  • 클라이언트는 ProxyImage와 RealImage의 차이를 전혀 알지 못하고 동일한 방식으로 사용.

프록시 패턴의 장점

  • 객체 생성 지연, 접근 제어, 추가 기능(로깅, 캐싱 등)을 쉽게 구현할 수 있음.
  • 실제 객체와 클라이언트 간의 결합도를 낮춰 유연성을 높임.
  • 네트워크 통신 또는 보안 요구 사항과 같은 복잡한 작업을 추상화.

프록시 패턴의 단점

  • 프록시 객체를 추가로 생성하므로 코드가 복잡해질 수 있음.
  • 요청을 실제 객체로 위임하므로 약간의 성능 저하가 있을 수 있음.
  • 잘못 설계하면 유지보수가 어려울 수 있음.

전략 패턴이 적합한 상황

  1. 알고리즘이 다양하게 변경될 수 있는 경우:
    • 실행 시점에서 서로 다른 알고리즘을 적용해야 하는 경우.
    • 예: 결제 방식, 경로 탐색 알고리즘, 데이터 압축 방식 등.
  2. 동작을 동적으로 변경할 필요가 있는 경우:
    • 실행 중에 객체의 동작을 쉽게 변경할 수 있어야 하는 경우.
  3. 코드의 중복을 줄이고 유지보수성을 높이고 싶은 경우:
    • 다양한 동작을 캡슐화하여 코드 변경을 최소화하고 확장을 용이하게 하고 싶을 때.