[Design patterns] Proxy Pattern (프록시 패턴)
Introduction
구조 시스템 패턴 시리즈의 완결까지 본 글을 포함해 두 편 남았다! 이번에 소개할 디자인 패턴은 Proxy 패턴이다. Proxy 패턴을 한줄 요약하면 다음과 같다.
어떤 오브젝트로의 접근을 중간에서 제어하기 위한 대리자 (proxy) 오브젝트를 제공한다.
본 글의 많은 부분은 에릭 감마의 GoF Design Pattern 서적에서 참고했고, 파이썬에 맞추어 살짝씩 내용을 변경한 부분이 있다.
Motivation
어떤 오브젝트의 생성 코스트가 너무 커서, 전체 시스템을 처음 구성할 때에 바로 생성하지 않고 그것이 실제로 필요할 때 까지 잠시 생성을 미루어야 할 때가 있다.
프록시 패턴은 어떤 오브젝트의 접근을 가로챈 후 그 오브젝트가 놓여야 할 곳에 대신 자리를 차지함으로써, 원래 오브젝트의 역할을 대신하거나 원래 오브젝트로의 접근을 중간에서 제어할 수 있다.
Applicability
프록시 패턴은 어떤 오브젝트에 대해서 단순 포인터에 비해 더 고도화된 방식의 접근이 필요할 때 유용하다. 예시를 들면 아래와 같다.
- 오브젝트의 생성 비용이 많이 드는 경우 (virtual proxy)
- 오브젝트에 대한 접근을 제한해야 하는 경우 (protection proxy)
- 오브젝트에 대한 참조 횟수를 추적하거나 캐시를 적용해야 하는 경우 (smart reference)
Structure
Proxy 패턴은 다음의 구성요소로 이루어져 있다.
- Proxy
- Real subject로의 접근을 위한 레퍼런스를 가지고 있다. 프록시는 RealSubject와 Subject의 인터페이스가 같을 경우 Subject의 레퍼런스를 가질 수도 있다.
- Subject와 동일한 인터페이스를 제공함으로써 (상속) 프록시는 real subject가 있어야 할 위치에 대체되어 쓰일 수 있다.
- Real subject로의 접근을 컨트롤하며 그것의 생성이나 삭제를 담당하기도 한다.
- 프록시의 종류에 따라 추가적인 책임 범위가 달라지는데, 예를 들어 virtual proxy는 실제 subject의 부가 정보를 캐싱해 둠으로써 subject로의 접근을 지연시키고, protection proxy는 접근자가 적절한 권한을 가지고 있는지 확인하는 역할을 한다.
- Subject
- Realsubject와 Proxy의 공통 인터페이스를 정의한다.
- RealSubject
- Proxy가 표현하고자 하는 실제 오브젝트를 정의한다.
Consequences
- Virtual proxy는 오브젝트를 실제로 필요로 할 때에만 생성함으로써 최적화를 수행할 수 있다. 혹은 무거운 오브젝트를 copy하라는 요청이 들어왔을 때, 실제 copy 작업을 copy된 오브젝트가 수정되는 시점까지 미루는 식으로 응용이 가능하다 (copy-on-write).
- Protection proxy와 smart reference는 오브젝트에 접근이 이루어질 때 추가적인 작업을 덧붙일 수 있다.
Implementation
Protection Proxy
class Car:
def __init__(self, driver):
self.driver = driver
def drive(self):
print(f'Car being driven by {self.driver.name}')
class CarProxy:
def __init__(self, driver):
self.driver = driver
self._car = Car(driver)
def drive(self):
if self.driver.age >= 16:
self._car.drive()
else:
print('Driver too young')
class Driver:
def __init__(self, name, age):
self.name = name
self.age = age
if __name__ == '__main__':
car = CarProxy(Driver('John', 12))
car.drive()
CarProxy 클래스는 실제 Car 오브젝트의 레퍼런스를 가지고 있으며, Car 오브젝트 바깥에서 그것의 접근을 제한하고 있다 (driver 나이가 16살 미만이면 drive 메서드를 호출할 수 없음). 이를 실제 문제에 적용하면, 현재 로그인 한 유저의 권한에 따라 기능 이용을 제한 한다던지로 응용될 수 있다.
여기서 공통 베이스클래스는 등장하지 않았는데, Car와 CarProxy의 인터페이스는 동일하게 유지되고 있음을 확인하자.
Virtual Proxy
class Bitmap:
def __init__(self, filename):
self.filename = filename
print(f'Loading image from {filename}')
def draw(self):
print(f'Drawing image {self.filename}')
class LazyBitmap:
def __init__(self, filename):
self.filename = filename
self._bitmap = None
def draw(self):
if not self._bitmap:
self._bitmap= Bitmap(self.filename)
self._bitmap.draw()
def draw_image(image):
print('About to draw image')
image.draw()
print('Done drawing image')
if __name__ == '__main__':
bmp = LazyBitmap('facepalm.jpg') # Bitmap
draw_image(bmp)
여기서는 LazyBitmap이 proxy 패턴에 해당한다. 이 클래스는 Bitmap과 같은 인터페이스를 가지며, Bitmap 클래스 오브젝트에 대한 레퍼런스를 가지고 있지만 이 오브젝트의 초기화는 draw 메서드가 호출되었을 때만 수행된다. Bitmap 오브젝트의 초기화에 굉장히 코스트가 많이 드는 경우, 실제로 해당 오브젝트를 필요로 하는 경우에만 (draw의 호출) 이를 수행하도록 초기화를 미루는 것이다.
Related Patterns
- Adapter 패턴은 어떤 오브젝트에 대해 또다른 인터페이스를 제공하지만, Proxy 패턴은 동일한 인터페이스를 제공하면서 접근에 대해 추가적인 동작을 수행한다.
- Decorator 패턴과 구현이 비슷하지만 decorator는 강화된 인터페이스를 제공하고 proxy는 동일한 인터페이스를 제공한다는 점이 다르다. 또한 decorator는 오브젝트에 몇 가지 추가적인 책임을 부여하지만 proxy는 단지 오브젝트의 바깥에서 오브젝트에 대한 접근을 통제한다.
- 또한 decorator는 항상 대상이 되는 오브젝트의 직접적인 레퍼런스를 가지고 있어야 하지만, proxy는 file name 이라던지 ID, 주소와 같이 오브젝트에 대한 간접적인 레퍼런스만을 가지고 있어도 된다.
아래는 참고할 만한 페이지들이다. Implementation 코드는 두 번째 링크의 강좌에서 참고했다.
References
Design Patterns 15 Years Later: An Interview with Erich Gamma, Richard Helm, and Ralph Johnson
https://www.informit.com/articles/article.aspx?p=1404056
Udemy: Design Patterns in Python