[Design patterns] Adapter Pattern (어댑터 패턴)
Introduction
이번 글에서는 structural pattern (구조 패턴) 중 하나인 어댑터 패턴에 대해 소개하고자 한다. 어댑터 패턴에 대해 요약하자면, 현재의 클래스 인터페이스 X를 요구되는 인터페이스인 Y로 변환해 주는 디자인 패턴이라 할 수 있다.
본 글의 많은 부분은 에릭 감마의 GoF Design Pattern 서적에서 참고했고, 파이썬에 맞추어 아주 살짝씩 변경한 부분이 있다.
Motivation
재사용을 위해 작성해놓은 툴킷 클래스가, 실제로 그것을 사용하는 쪽에서 요구하는 특수한 인터페이스를 가지고 있지 않아 사용이 불가능한 경우가 있다.
이런 상황에서 여기저기서 재사용되는 기존의 툴킷 클래스의 소스 코드를 변경하지 않고 현재의 특수한 인터페이스 요구 상황에 맞추기 위해 어댑터 패턴을 사용할 수 있다.
Applicability
어댑터 패턴은 다음과 같은 상황에서 쓰일 수 있다.
- 어떤 클래스가 지금 필요로 하는 인터페이스를 가지고 있지 않을 때 사용할 수 있다.
- 서로 인터페이스를 공유하지 않는 여러 클래스에 재사용되어야 할 클래스를 작성할 때 사용할 수 있다.
- (object adapter) 특정 인터페이스를 공유하는 여러 서브클래스의 인터페이스를 단 하나의 어댑터로 변환할 수 있다.
Structure
어댑터의 구현으로는 다중 상속을 이용하는 class adapter와 object composition을 이용하는 object adapter가 있다.
- Target: 클라이언트가 사용하는 특수한 인터페이스가 정의된 클래스이다.
- Client: 타겟과 동일한 인터페이스를 공유하는 오브젝트와 호환되는 클래스이다.
- Adaptee: 타겟과 다른 인터페이스를 가지고 있는 클래스이다.
- Adapter: Adaptee의 인터페이스를 타겟 인터페이스로 변환 (adapt) 시켜주는 클래스이다.
Class adapter
클래스 어댑터는 타겟과 Adaptee를 다중 상속함으로써 구현된다. 위 그림에서는, 다중 상속된 Adapter 클래스에서 request() 를 호출 시 Adaptee 클래스로부터 상속한 specifig_request() 메서드를 호출함으로써 Target 클래스에서 정의된 인터페이스를 구현하고 있다. 위 그림에서 삼각형이 붙은 화살표는 클래스 간의 상속 관계를 표현한다.
다중 상속을 하고 있기 때문에 인터페이스를 맞추는 과정에서 Adaptee의 인터페이스를 유지하지 않고 무시하는 쪽으로 오버라이드 할 수 있다.
Object adapter
오브젝트 어댑터는 Adaptee 를 상속하는 대신 Adaptee 오브젝트를 직접 Adapter 클래스가 프로퍼티의 형태로 들고 있고, Target 인터페이스에 맞는 메서드가 호출되면 Adapter가 직접 Adaptee 오브젝트에서 적절한 인터페이스 호출을 통해 원하는 작업을 수행하게 된다. 위 그림에선, Adapter 클래스가 Adaptee 클래스 오브젝트를 adaptee 이름으로 가지고 있고, request() 메서드 호출 시 self.adaptee.specific_request() 메서드를 직접 호출하게 된다.
Consequences
클래스 어댑터와 오브젝트 어댑터를 사용함으로써 얻을 수 있는 효과는 다음과 같다.
Class adapter
- 구체적인 Adaptee 클래스를 상속받기 때문에 Adaptee 하위의 모든 서브클래스를 위해 작동하는 어댑터를 구현할 수 없다.
- 상속으로 인해 어댑터에서 Adaptee의 동작을 오버라이드 할 가능성이 존재한다.
- 단일 오브젝트로 이루어져 있어 Adapter-Adaptee의 접근 과정을 거치지 않고 Adaptee 자체에 직접 접근할 수 있다.
Object adapter
- 특정한 Adaptee와 그것을 상속받는 모든 서브클래스에 대해 작동하는 어댑터 구현이 가능하다.
- Adaptee의 동작을 오버라이드 하기 어려워지는데, 만약 필요한 때에는 Adaptee를 서브클래싱 하고 그걸 가져다 쓰는 어댑터를 구현해야 한다.
Implementation
아래는 파이썬에서 해당 디자인 패턴을 적용했을 때의 대략적인 사용 방식이다. 아래 코드는 실제 실행시켜 볼 수는 없고, 적용 시의 느낌만을 보여주기 위한 수도코드이므로 돌게 하려 노력할 필요는 없다...
from abc import ABC, abstractmet
from typing import List, Tuple
class Shape(ABC):
"""Target class for interface definition."""
@abstractmethod
def get_bounding_box(self):
pass
class Line(Shape):
"""Some class inheriting target class, sharing same interface."""
def __init__(self, start, end):
self.start = start
self.end = end
def get_bounding_box(self) -> List[int]:
x_min = min(self.start.x, self.end.x)
x_max = max(self.start.x, self.end.x)
y_min = min(self.start.y, self.end.y)
y_max = max(self.start.y, self.end.y)
return [x_min, x_max, y_min, y_max]
class TextView:
"""Some class with incompatible interface with target class."""
def __init__(self, textview_init_params):
...
def get_extent(self) -> Tuple[int]:
return ...
# Class adapter example.
class TextShape1(Shape, TextView):
"""Adapter class converting adaptee interface into target interface."""
def __init__(self, textview_init_params):
super(TextView, self).__init__(textview_init_params)
def get_bounding_box(self) -> List[int]:
return list(self.get_extent())
# Object adapter example.
class TextShape2:
"""Adapter class converting adaptee interface into target interface."""
def __init__(self, textview_init_params):
self.text: TextView = TextView(textview_init_params)
def get_bounding_box(self) -> List[int]:
return list(self.text.get_extent())
Shape 추상 클래스에서 먼저 인터페이스를 정의해 주었고, Line 구체화 클래스는 해당 인터페이스를 상속받아 실제 기능을 구현하고 있다. TextView 클래스는 위 다이어그램에서의 Adaptee에 해당하는 클래스로, Shape 클래스에서 정의된 get_bounding_box 메서드가 구현되어 있지 않다.
TextShape1 와 TextShape2 클래스는 어댑터 패턴을 적용해 TextView 클래스의 인터페이스를 Shape 클래스의 인터페이스로 변환해 주는 역할을 한다. 전자는 클래스 어댑터 패턴을, 후자는 오브젝트 어댑터 패턴을 적용한 예시이다. 파이썬 언어 기준으로는 오브젝트 어댑터 패턴을 사용하는 쪽이 훨씬 자연스러운 것 같다.
Notes
- 어댑터가 실질적으로 수행해야 하는 작업의 양은 타겟과 adaptee 클래스 간의 인터페이스가 얼마나 차이나냐에 따라 달라진다.
- Two-way 어댑터는 기존의 adaptee가 사용하던 인터페이스를 그대로 유지함으로써, 타겟 인터페이스와 adaptee 인터페이스 둘 다 사용 가능한 상태로 만드는 디자인 패턴이다. 어떤 오브젝트를 여러 클라이언트에서 각각 다른 인터페이스를 사용해 접근해야 할 때 쓸 수 있다.
- 두 인터페이스에 공통점이 많지 않은 경우에 다중 상속을 통해 각각의 인터페이스를 유지하는 방향으로 two-way 어댑터를 구현할 수 있다.
Related Patterns
- Bridge 패턴과 object adapter는 비슷하지만, 전자는 어떤 기능에 그것을 편하게 사용할 수 있도록 돕는 인터페이스를 선택적으로 조합하는 방식이라면 후자는 이미 존재하던 인터페이스를 다른 인터페이스로 변환하는 것이 의의이다.
- Decorator 패턴은 다른 오브젝트의 기능을 강화하지만 인터페이스 변경은 수행하지 않는다. 따라서 Adapter와는 다르게 recursive composition이 가능하다 (데코레이트 한 오브젝트를 다시 데코레이트 할 수 있음).
- Proxy 패턴은 다른 오브젝트의 인터페이스를 그대로 가지며 해당 오브젝트가 실제로 필요해지기 전까지 해당 오브젝트의 역할을 대신할 수 있다.
아래는 참고할 만한 사이트들이다.
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/