Introduction
구조 시스템 패턴을 다루는 과정에서 패턴 이름들이 알파벳 순서로 나열되었던 게 좋았었는데 (Adapter, Bridge, Composite, Decorator), 이번엔 E로 시작하는 패턴이 나올 차례지만 안타깝게도 그런 디자인 패턴은 없다... 이번 글에서는 눈물을 머금고 알파벳 한 자리를 건너뛰어 Facade 패턴에 대해 소개하고자 한다.
Facade 패턴을 한줄 요약하면 다음과 같다.
복잡한 시스템을 더 쉽게 사용하기 위한 high-level의 인터페이스를 제공한다.
본 글의 많은 부분은 에릭 감마의 GoF Design Pattern 서적에서 참고했고, 파이썬에 맞추어 아주 살짝씩 변경한 부분이 있다.
Motivation
시스템을 여러 서브시스템으로 나누는 식으로 전체 시스템의 복잡도를 낮출 수 있는데, 여기서 중요하게 고려해야 하는 것 중 하나는 각 서브시스템 사이의 통신과 의존성을 최소화 하는 것이다. Facade 오브젝트는 서브시스템의 기능을 사용하기 위한 단순화된 인터페이스를 제공하는 식으로 위 상황에 대한 해결책으로 작용할 수 있다.
Applicability
- 복잡한 서브시스템에 대한 간단한 인터페이스를 제공하고 싶을 때 사용할 수 있다. 대부분의 디자인 패턴들은 서브시스템을 더 작고 다양하게 만들어 주는데, 이는 시스템을 더 수정하기 편하고 재사용이 편하게 해 주지만 이런 추가적인 수정이 필요하지 않은 클라이언트에게는 사용성을 떨어트릴 뿐이다. Facade 패턴은 서브시스템에 대한 간단한 default view를 제공함으로써 대부분의 클라이언트가 사용하기에 충분한 인터페이스를 제공한다.
- 클라이언트와 구체화 클래스들 사이에 많은 dependency가 존재할 때 유용하다. 클라이언트나 다른 서브시스템에 대한 의존성을 끊어내 서브시스템의 독립성을 강화할 수 있다.
- 서브시스템 레이어링에 이용할 수 있다. 서로 의존하는 서브시스템들이 존재할 때, 오직 Facade를 통해서만 서로의 통신이 일어나도록 만들어 Facade가 각 서브시스템 레벨에 대한 진입점 역할을 하게 할 수 있다.
Structure
Facade 패턴은 다음의 구성요소로 이루어져 있다.
- Facade
- 요청이 왔을 때, 어느 서브시스템이 답해야 하는 지 알고 있다.
- 클라이언트 요청에 대한 작업을 적절한 서브시스템 오브젝트에 맡긴다.
- Subsystem classes
- 서브시스템 기능을 구현한다.
- Facade 오브젝트에 의해 맡겨진 작업을 수행한다.
- Facade의 존재에 대해 알지 못 한다.
Consequences
- 클라이언트가 직접 서브시스템에 접근하는 것을 막아 클라이언트가 다루어야 할 오브젝트의 갯수를 줄이고, 결국 서브시스템을 사용하는 것이 편해진다.
- 클라이언트와 서브시스템을 약한 결합 (weak coupling) 으로 묶고 있어 클라이언트에 영향을 주지 않고도 서브시스템 요소를 변경할 수 있다.
- 시스템이나 오브젝트 간의 의존성을 레이어링 할 수 있다.
- 복잡한 의존 관계나 순환 의존성을 없애는 데 도움을 준다.
- 클라이언트가 서브클래스를 직접 이용하는 것을 금지하지는 않기 때문에, 클라이언트 쪽에서는 편한 인터페이스를 사용할지 말지 선택할 수 있다.
Implementation
class Buffer:
"""Subsystem class 1."""
def __init__(self, width=30, height=20):
self.width = width
self.height = height
self.buffer = [' '] * (width*height)
def __getitem__(self, item):
return self.buffer.__getitem__(item)
def write(self, text):
self.buffer += text
class Viewport:
"""Subsystem class 2."""
def __init__(self, buffer=Buffer()):
self.buffer = buffer
self.offset = 0
def get_char_at(self, index):
return self.buffer[self.offset+index]
def append(self, text):
self.buffer += text
class Console:
"""Facade pattern class."""
def __init__(self):
b = Buffer()
self.current_viewport = Viewport(b)
self.buffers = [b]
self.viewports = [self.current_viewport]
# high-level
def write(self, text):
self.current_viewport.buffer.write(text)
# low-level
def get_char_at(self, index):
return self.current_viewport.get_char_at(index)
if __name__ == '__main__':
c = Console() # Facade class initialization.
c.write('hello') # High-level subsystem operation.
ch = c.get_char_at(0) # Low-level subsystem operation.
Base에 다이아몬드, head에 점이 붙은 화살표는 base 쪽이 head를 aggregate 하고 있다는 뜻이다 (리스트 등으로 가지고 있음). 점선 화살표는 오브젝트를 가지고 있으며, 초기화까지 진행한다는 뜻이다. 검은 박스는 code snippet을 나타낸다. Udemy: Design Patterns in Python 의 소스코드를 대부분 참고했다.
Facade 오브젝트에 해당하는 Console 클래스가 작업 요청을 처리하기 위해 필요한 서브클래스 오브젝트를 가지고 있으며, 클라이언트로부터 어떤 작업을 요청받았을 때 적절하게 내부의 서브클래스의 동작을 이용해 처리해 주고 있다. 클라이언트 쪽에서는 내부에 어떤 서브클래스가 존재하는 지는 전혀 신경 쓸 필요 없이, Facade 클래스의 인터페이스만을 이용하게 된다.
또한 이 상황에서, 클라이언트는 시스템 내부 구조를 알고 있다면 직접 서브클래스 메서드를 호출할 수도 있다. 그리고 특정 요청에 대한 동작 구현이 달라져야 할 때에도, 클라이언트가 high-level의 Facade 인터페이스만을 사용하는 경우 클라이언트의 시스템 이용에 아무런 영향을 주지 않으면서 동작을 수정할 수 있다.
Related Patterns
- Abstract Factory 패턴을 이용해 subsystem-independent하게 서브시스템 오브젝트를 생성하도록 할 수 있다.
- Mediator 패턴은 이미 존재하는 클래스의 기능을 추상화 한다는 점에서 유사하지만, 오브젝트 간의 통신을 추상화한다는 점에서 다르다. 각 오브젝트는 다른 오브젝트와 직접 통신하는 대신 Mediator라는 것의 존재를 알고 Mediator를 통해 간접적으로 통신하게 되며, 종종 Mediator는 어떤 클래스에도 속하지 않는 기능을 제공하기도 한다. 반대로 Facade에서는 기능 추가 없이 인터페이스만을 제공하고, 각 서브시스템 클래스는 Facade의 존재를 알지 못한다.
- Facade 오브젝트는 하나만 필요한 경우가 잦기 때문에, 종종 Singleton 으로 구현되기도 한다.
아래는 참고할 만한 페이지들이다.
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
https://www.udemy.com/course/design-patterns-python/