Introduction
이번 글에서는 또다른 structural pattern (구조 패턴) 인 브릿지 패턴에 대해 소개하고자 한다. 브릿지 패턴은 인터페이스와 실제 구현을 분리해, 각각이 독립적으로 수정될 수 있도록 도와준다.
본 글의 많은 부분은 에릭 감마의 GoF Design Pattern 서적에서 참고했고, 파이썬에 맞추어 아주 살짝씩 변경한 부분이 있다.
Motivation
인터페이스는 동일하게 유지한 채로, 실제 기능은 여러 가지 방식으로 구현하고 싶을 땐 다음과 같은 방법을 사용할 수 있다.
먼저 추상 클래스 (abstract class) 를 정의해 인터페이스를 설정한다 (추상화, abstraction). 이후엔, 그것을 상속하는 실체 서브클래스 (concrete subclasses) 들에선 해당 인터페이스를 이용해 실제로 수행해야 하는 기능을 클래스마다 제각기 다른 방식으로 구현한다. 이런 식으로 동일 인터페이스를 공유하되 구현은 다른 여러 개의 클래스를 만들 수 있다.
예를 들면, 어떤 어플리케이션이 서로 다른 두 환경에서 동일한 인터페이스로 작동할 수 있어야 한다면, 각 환경에 맞는 기능을 하는 서브클래스를 각각 만들어 인터페이스는 유지한 채로 서로 구현을 달리할 수 있다.
하지만 이럴 경우 문제가 생기는데, 일단 서브클래스를 만들고 나면 추상화와 구현이 완전히 결합되어 더 이상 서로를 자유롭게 결합시키는 것이 불가능하다. 하나의 예시를 보자.
추상 클래스를 통해 하나의 abstraction을 공유하며, 각기 다른 환경에서 작동하기 위한 두 구현을 실체 클래스로 작성했다고 하자. 문제 없이 인터페이스도 공유하며 구현도 달리할 수 있다. 하지만 이 시점에서, 기존 추상화에 조금의 인터페이스를 더한 새로운 추상 클래스를 정의해야 한다고 해 보자. 아래의 그림을 보자.
기존의 실체 클래스 구현은 새로운 추상 클래스의 구현으로 재사용 할 수 없기 때문에 (즉, 기존의 추상화와 구현이 서로 바인딩 되어 있는 상태이다) 새 추상 클래스에 대해서 두 개의 실체 구현을 다시 해야 한다.
결국 하나의 추상 클래스를 새로 작성하면서 두 개의 실체 클래스를 또 만들게 되었으며, 만약 새 추상화나 새 환경이 추가된다면 새로 작성해야 하는 실체 클래스의 수는 끝없이 늘어나게 될 것이다.
이에 대한 해결책으로는 클래스 계층을 인터페이스와 구현 각각에 대해 처음부터 따로 만들어 두고, 각각을 연결해 사용함으로써 추상화와 구현을 서로 독립적인 관계로 유지할 수가 있는데, 이것을 Bridge 패턴이라 한다.
Applicability
브릿지 패턴은 다음과 같은 상황에서 유용하다.
- 추상화와 구현이 서로 완전히 결합 (binding) 되는 것을 피하고 싶을 때 사용할 수 있다. 예를 들면, 인터페이스는 유지한 채로 실제 작동 방식은 런타임 중에도 자유롭게 변경하고 싶을 때 사용하면 좋다.
- 추상화와 구현 모두가 서브클래싱을 통해 확장되어야 할 때 쓸 수 있다. Bridge 패턴을 사용해 추상화와 구현을 자유롭게 결합해 사용할 수 있다.
- 코드 재 컴파일 없이도 동일 인터페이스에 대한 기능을 변경해 사용할 수 있어야 할 때 쓸 수 있다.
- 동일한 기능 구현이 여러 다른 오브젝트에 의해 공유되어야 하고, 이 사실이 클래스를 사용하는 쪽에는 감춰져야 할 때 사용할 수 있다.
Structure
각 구성 요소의 설명은 아래와 같다.
- Abstraction: 추상 인터페이스를 정의하는 클래스이다.
- RefinedAbstraction: Abstraction에 의해 정의된 인터페이스를 확장한다.
- Implementor: 실제 기능을 구현하는 클래스의 인터페이스를 정의하는 클래스이다. Abstraction 클래스의 인터페이스와 일치할 필요는 없다. 하지만 Implementor에서는 low-level의 operation을, Abstraction에서는 그것들을 이용하는 high-level의 operation을 정의하는 것이 일반적이다.
- ConcreteImplementor: Implementer 클래스의 인터페이스를 구체화 하는 실체 클래스이다.
Abstraction이 Implementor를 프로퍼티로 들고 있지만, 이 Abstraction은 Implementor의 실제 구현이 어떻게 돌아가고 있는 지는 신경쓰지 않는다. 단지 Implementor가 정의해 놓은 인터페이스만을 보고 사용할 뿐이다.
이런 상황에서, Abstraction의 확장 (RefinedAbstraction) 이나 Implementor의 실제 구현 (ConcreteImplementorA,B) 은 위 구조를 망가트리지 않고 얼마든지 일어날 수 있다.
Consequences
브릿지 패턴을 사용함으로써 얻을 수 있는 효과는 아래와 같다.
- 구현이 인터페이스에 코드의 형태로 완전히 결합되지 않고 분리되며, 이를 통해 런타임 시 추상화에 대한 구현 방식을 결정하거나 심지어 런타임 중에 구현을 변경할 수 있다. 또한, 구현 변경 시에 코드의 재컴파일을 필요로 하지 않는다.
- 그리고 high-level 기능을 쓰는 쪽에서는 세부적인 확장 내용을 알 필요가 없고 단지 Abstraction과 Implementor의 정보만 알면 된다는 장점이 있다.
- Abstraction과 Implementor의 계층 구조를 독립적으로 확장시킬 수 있다. 추상 클래스에 대한 실체 클래스를 하나하나 만들던 기존 방식에서는 한 개의 추상화가 추가될 때 마다 N개의 구현에 대해 서브클래스를 새로 만들어 줘야 했지만, Bridge 패턴을 이용하면 그럴 필요가 없다.
- 클라이언트로부터 구현에 대한 자세한 디테일을 숨길 수 있다.
Implementation
from abc import ABC, abstractmethod
class Renderer(ABC):
"""Implementor class."""
@abstractmethod
def render_circle(self, center, radius):
pass
class PilRenderer(Renderer):
"""Concrete implementation of Implementor"""
def render_circle(self, center, radius):
print("Drawing circle with Pillow.")
class Cv2Renderer(Renderer):
"""Another concrete implementation of Imlementor"""
def render_circle(self, center, radius):
print("Drawing circle with OpenCV.")
class Shape(ABC):
"""Abstraction class with rendering feature."""
def __init__(self, renderer: Renderer):
self.renderer = renderer
@abstractmethod
def draw(self):
pass
@abstractmethod
def resize(self, factor):
pass
class Circle(Shape):
"""Concrete class of abstraction class Shape."""
def __init__(self, renderer: Renderer, center, radius):
super().__init__(renderer)
self.center = center
self.radius = radius
def draw(self):
self.renderer.render_circle(self.center, self.radius)
def resize(self, factor):
self.center *= factor
self.radius *= factor
def move_center(self, new_center):
self.center = new_center
클라이언트와 협업하는 Shape 추상 클래스가 있고, 도형 렌더링 기능의 인터페이스를 담당하는 Renderer 추상 클래스가 있다. 이 둘은 각각 다른 계층 구조를 가지며, Shape 클래스 쪽에서는 Renderer의 오브젝트를 프로퍼티로 가지고 있는 형태로 렌더링 기능에 필요한 구현을 가져다 사용하고 있다.
여기서 Shape 클래스는 Renderer의 인터페이스만 알고 있으면 되고, 실제로 어떤 세부 구현이 들어가 있는 지는 관심을 갖지 않는다. 이것은 Shape 클래스를 상속하는 Circle 클래스에도 마찬가지로 적용되며, Circle 클래스에서도 renderer에서 render_circle() 메서드를 호출하기만 할 뿐 Renderer의 세부 구현 내용과는 독립적으로 동작한다.
위 코드를 클래스 다이어그램으로 표현하면 아래와 같다.
세모가 붙은 선은 상속 관계를 나타내며, 검정 박스는 해당 메서드가 파이썬으로 구현되었을 때의 code snippet 예시이다. 마름모가 붙은 화살표는 base 쪽이 head쪽 오브젝트를 특정 이름으로 가지고 있다는 의미이다.
Notes
- 기능에 대한 한 가지의 구현만 존재할 때에는 사실 Implementor에 해당하는 추상 클래스가 꼭 필요하진 않지만, 구현의 변경 사항이 그 기능을 사용하는 클라이언트 쪽에 영향을 미치지 않게 막고 싶다면 이런 상황에서도 여전히 Abstraction과 Implementor의 분리는 유용하게 쓰일 수 있다.
- Abstraction 초기화 시 적절한 Implementor를 선택해 함께 초기화 하도록 구현할 수도 있지만, 일단 디폴트 Implementor로 초기화 해 놓은 후 상황에 따라 더 나은 implementation으로 변경하도록 할 수도 있다.
Related Patterns
- Abstract Factory 패턴을 이용해 Bridge 를 생성할 수 있다.
- Adapter 패턴 (그 중 object adapter) 과 구조가 유사하지만 서로 의도하는 목적은 다르다. Adapter는 서로 호환되지 않는 인터페이스를 맞추기 위함이고, Bridge를 쓰는 목적은 인터페이스와 기능을 분리해 서로 독립적으로 변경될 수 있도록 하기 위함이다.
아래는 참고할 만한 페이지들이다.
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/