의도
클래스의 인터페이스를 사용자가 기대하는 인터페이스 형태로 적응(변환) 시킨다.
다른 이름
Wrapper(래퍼)
동기
"프로그램이 요청하는 인터페이스 ≠ 툴킷에 정의된 인터페이스" 인 경우 툴킷이 재사용을 목표로 개발되었다고 해도 실제 재사용되지 못할 수가 있다.
그림 편집기를 예로 들어보자.
그림판의 중요한 추상적 개념은 그래픽 객체들이다. 공통된 그래픽 요소에 대한 인터페이스를 추상 클래스 Shape 이라고 정의한다. 그리고 각각의 그래픽 요소들을 Shape의 서브클래스로 정의한다. 즉, 선을 위한 LineShape, 다각형을 위한 PolygonShape 등을 Shape을 상속받아 개발하면 된다.
LineShape이나 PolygonShape 과 같은 매우 기본적인 그래픽 요소들은 구현이 비교적 쉽지만, TextShape은 다른 요소들에 비해 구현이 어렵다. 텍스트를 처리할려면 화면 수정, 버퍼 관리 등 복잡한 기능을 모두 구현해야 하기 때문이다. 그래서 기존에 출시되어 있는 사용자 인터페이스 툴킷 중 복잡한 TextView를 처리하는 TextView 클래스를 재사용할 것이다. 그러나 기존 툴킷들은 지금 새롭게 개발한 Shape 클래스를 고려한 것이 아니기 때문에 바로 TextView 클래스를 TextShape으로 사용할 수는 없다.
그렇다면 TextView와 같이 이미 존재하지만 현재 이를 사용하고자 하는 클래스와는 아무 연관 없이 개발된 클래스이거나, 서로 일치하지 않는 인터페이스를 갖는 클래스들을 잘 통합하여 하나의 응용프로그램을 개발해야 할 때 어떻게 해야 할까?
→ 기존에 만들어진 TextView의 인터페이스를 변경하여 Shape의 인터페이스와 일치하게 만들 수 없을까?
만약 툴킷의 소스 코드를 갖고 있다면 당연히 가능하지만, 소스가 없다면 TextView 자체를 변경할 수 없다. 이미 개발된 클래스의 인터페이스를 바꿀 수 없다면 Shape과 TextView 인터페이스에 둘 다 맞도록 우리가 만든 TextShape 클래스를 조정해야 한다.
이를 위해 취할 수 있는 방법은 2가지 이다.
클래스 적응자 패턴 : Shape의 인터페이스와 TextView의 구현을 모두 상속받는다.(다중 상속)
객체 적응자 패턴 : TextView의 인스턴스를 TextShape에 포함시키고(객체 합성), TextView 인터페이스를 사용하여 TextShape을 구현한다.
이때, TextShape을 Adapter(적응자)라고 한다.
TextShape::BoundingBox()
의 구현을 보면, 실제 구현을 제공한 TextView에 정의된 TextView::GetExtent()
를 호출하도록 바꾸어 TextView에 전달한하고 있다. 이렇게 TextShape는 TextView에 정의된 인터페이스를 바꾸어(GetExtent()
→ BoundingBox()
) Shape에 정의된 인터페이스와 부합되게 처리함으로써, 그림 편집기는 인터페이스가 불일치되는 문제가 있던 TextView 클래스를 TextShape 적응자를 통해 재사용할 수 있게 되었다.
가끔 적응자(Adapter)는 적응 대상 클래스(Adaptee)가 제공하지 않는 기능을 제공해야 하는 책임을 지니게 된다. 앞의 그림에서, TextView는 Shape 객체를 다른 위치로 드래그할 수 있는 기능을 제공하지 않는다. 그래서 TextShape에서 CreateManipulator()
에 드래그를 처리하는 행동을 추가로 정의하여 TextView에 빠진 기능을 제공한다.
연산 내부를 보면 TextManipulator 객체를 리턴하는데, 이는 Manipulator의 서브클래스이다. Manipulator는 사용자 요청에 따라 Shape 인스턴스를 어떻게 움직여야 하는지를 알고 있는 추상 클래스이다. 그래서 각 그래픽 요소별로 이를 상속받는 서브클래스를 재정의해야 한다. TextManipulator는 TextShape에 대응되는 클래스이고, TextShape은 TextManipulator의 인터페이스를 반환하여 TextShape에는 없지만 Shape가 요구하는 기능을 추가한다.
활용성
기존 클래스를 사용하고 싶은데 인터페이스가 맞지 않을 때
아직 예측하지 못한 클래스나 실제 관련되지 않는 클래스들이 기존 클래스를 재사용하고자 하지만, 이미 정의된 재사용 가능한 클래스가 지금 요청하는 인터페이스를 꼭 정의하고 있지 않을 때
- 즉, 이미 만든 것을 재사용하고자 하나 이를 수정할 수 없을 때
[객체 적응자만 해당됨] 이미 존재하는 여러 개의 서브클래스를 사용해야 하는데, 이 서브클래스들의 상속을 통해서 이들의 인터페이스를 다 바꾸는 것이 현실성이 없을 때
- 객체 적응자를 사용하여 부모 클래스의 인터페이스를 변형하는 것이 바람직함
구조
- 클래스 적응자 : 다중 상속을 활용하여 한 인터페이스를 다른 인터페이스로 적응(adapt) 시킨다.
- 객체 적응자 : 객체 합성을 사용
참여자
Target
사용자가 사용할 응용 분야에 종속적인 인터페이스를 정의함.
Shape
가 해당.
Client
Target
인터페이스를 만족하는 객체와 동작할 대상.DrawingEditor
가 해당.
Adaptee
인터페이스의 적응이 필요한 기존 인터페이스를 정의하는 클래스.
TextView
가 해당.Service
라고도 함. 3rd party 혹은 legacy code일 수 있음.
Adapter
Target 인터페이스에 Adaptee의 인터페이스를 적응시키는 클래스
TextShape
가 해당
협력 방법
사용자는 적응자(adapter)에 해당하는 클래스의 인스턴스에게 연산을 호출함.
적응자는 해당 요청을 수행하기 위해 적응대상자(adaptee)의 연산을 호출.
user → client → adapter → adaptee
결과
클래스 적응자
Adapter
는Adaptee
를Target
으로 변형하는데, 이를 위해서Adaptee
를 상속받아야 하기 때문에,Adaptee
와 그 서브클래스들을 모두 개조할 생각이면 클래스 적응자 방식을 채택할 수 없다. 즉,Adapter
는 명시적으로Adaptee
를 상속받고 있을 뿐, 그 서브클래스들은 상속받은게 아니기 때문에Adaptee
의 서브클래스에 정의된 기능을 사용할 수 없다.Adapter
는Adaptee
를 상속하기 때문에Adaptee
에 정의된 행동(연산)을 재정의할 수 있다는 장점이 있다.한 개의 객체(
Adapter
)만을 사용하며,Adaptee
의 기능을 사용하기 위한 추가적인 포인터 간접화가 필요하지 않다.
객체 적응자
Adapter
하나만 존재해도 수많은Adaptee
와 동작할 수 있다.Adapter
가 포함하는Adaptee
참조자를 통해Adaptee
뿐 아니라,Adaptee
의 서브클래스도 관리할 수 있게 된다. 즉,Adaptee
계열의 모든 클래스들을 이용할 수 있게 된다.
Adaptee
의 행동(연산)을 재정의하기 매우 어렵다.- 하려면,
Adaptee
를 상속받아 새로운 서브클래스를 만든 후,Adapter
가 이 클래스를 참조하도록 하면 된다.
- 하려면,
고려해야 할 추가 사항들
Adapter
가 실제 적응 작업을 위해 들어가는 비용이 얼마나 되나?
이는 적응을 어떻게 할 것이냐에 따라 다르다. 가장 단순한 예는, 그냥 Adaptee
에 정의된 내용을 사용자가 사용할 Target
인터페이스에 적응시키는 것이다.(단순히 연산의 이름을 변경하는 정도?) 좀 더 복잡하게 하면, 새로운 연산을 추가하여 새로운 기능을 넣을 수 있을 것이다. 즉, 작업량을 결정 짓는 요인은 Target
과 Adaptee
간에 얼마만큼의 유사성이 있는가이다.
- 대체 가능한(Pluggable) 적응자
클래스의 재사용성을 높이려면, 누가 이 클래스를 사용할지에 대한 생각을 최소화 해야 한다. 만약 인터페이스의 변경이 필요하다면, 그냥 클래스를 하나 더 만들어 해결하게 함으로써 모든 사용자를 만족하는 인터페이스를 만들어야 한다는 부담감을 덜 수 있는 것이다.
→ 즉, 적응자 패턴을 통해 내가 개발한 클래스를 사용할 모든 사용자에게 동일한 인터페이스를 제공해야한다는 부담감을 덜어내므로써 클래스의 재사용성을 오히려 높일 수 있다. 다른 인터페이스를 원하는 사용자가 있다면, 그 사람이 적응자 클래스를 만들면 되는 것이다. 인터페이스 개조를 담당하는 이런 클래스를 대체 가능 적응자(pluggable adapter)라고도 한다.
예시
TreeDisplay
위젯은 트리 구조를 그래픽적으로 보여주는 것. 어떤 특정 응용 프로그램에서 사용하는 특수 목적의 위젯.TreeDisplay
위젯이 디스플레이할 객체들은 특정 인터페이스를 갖도록 해야 함. 이 객체들은Tree
추상 클래스를 상속해야 함.이를 사용하는 응용프로그램은 디렉토리 구조가 될 수도, 클래스 구조가 될 수도 있음. 이들은 각각 다른 인터페이스를 가짐.GetSubdirectories()
,GetSubclasses()
등으로 표현됨.
TreeDisplay
위젯의 재사용이 가능하려면 서로 다른 인터페이스를 갖는 두 종류의 계층 구조라도 이들을 화면에 표시할 수 있어야 함.TreeDisplay
는 자신이 아는Tree
클래스의 인터페이스에 맞도록 이 인터페이스들을 적응시켜야 함.
- 양방향 적응자를 통한 투명성 제공
적응자의 잠재적인 문제는 적응자가 모든 사용자한테 투명하지 않다는 것이다. 객체 적응자의 경우, 적응된 객체(Adapter
)는 Adaptee
의 인터페이스를 만족하지 않는다. 왜냐하면 Target
의 인터페이스만 상속받았고, Adaptee
의 인터페이스는 객체 합성을 통해 사용하기 때문이다.
이때, Target
만 필요한 사용자라면 상관이 없지만, Adaptee
객체를 통해 Target
을 사용해야 하는 사용자라면 적응된 객체(Adapter
)를 사용할 수 없다. 양방향 적응자(two-way adapter)는 이 두 경우를 모두 지원해야 한다. 즉, 서로 다른 두 사용자가 객체를 서로 다르게 바라봐야 할 때 필요한 기능이다. 밑의 예시를 보면 확 와닿을 것이다.
예시
Unidraw 그래픽 편집기 프레임워크와 QOCA 제약 해결용 툴킷은 서로 변수를 명시적으로 표현하는 클래스가 다르다.
Unidraw는
StateVariable
변수를 정의, QOCA는ConstraintVariable
변수를 정의하는데, Unidraw와 QOCA가 함께 동작하려면,ConstraintVariable
→StateVariable
로 적응시켜야 한다. 그리고 QOCA가 무엇인가의 처리를 Unidraw로 전달하려면 반대로StateVariable
→ConstraintVariable
로 적응시켜야 한다.즉, 첫번째 경우는
ConstraintVariable
이Adaptee
,StateVariable
이Target
이 되어야 하고, 두번째 경우는StateVariable
이Adaptee
,ConstraintVariable
이Target
이 되어야 한다.이렇게 두 클래스를 양방향으로 적응시키고 싶을 때,
ConstraintStateVariable
이라는 양방향 적응자를 둔다.
- 양방향 적응자는 개조되는 두 클래스의 인터페이스를 모두 상속받아 정의하도록 하여 구현한다.
구현
- 클래스 적응자를 C++로 구현하기
C++에서 Adapter
클래스는 Target
→ public
으로 상속, Adaptee
→ private
으로 상속받아야 한다. 즉, Target
의 인터페이스는 외부에 공개가 되지만, Adaptee
는 내부구현에 필요한 것이기 때문에 Adaptee
가 사용자에게 알려질 필요가 없다. 자세한 것은 항목 40: 다중 상속은 심사숙고해서 사용하자 (tistory.com) 참고
- 대체 가능 적응자
TreeDisplay
문제를 해결하는 적응자를 구현하는 방법은 3가지로 구분된다.
우선, Adaptee
에 정의된 인터페이스들 중 적응에 필요한 연산의 최소 집합을 따로 빼서 만든다. 이유는 수 십개의 연산을 갖는 인터페이스를 적응시키는 것 보다, 한 두개의 인터페이스만을 적응시키는 것이 훨씬 쉽기 때문이다. 최소한의 인터페이스가 정의된 Adapter
는 아마도 두 개의 연산을 가질 것이다.
CreateGraphicNode
: 계층 구조를 어떻게 그래픽적으로 노드화하여 보여줄 것인가를 정의한 연산GetChildren
: 어떻게 이 노드의 자식 노드들을 검색할 것인가를 정의한 연산
두 연산으로 좁혀진 최소 인터페이스로 다음 세 가지 구현 방법을 생각해보자.
- 추상 연산을 사용하는 방법
범위가 제한된 Adaptee
인터페이스를 추상 연산으로 TreeDisplay
클래스에 정의한다. 이 클래스를 상속받는 서브클래스는 이 추상 연산에 대한 구현을 제공해야 하고, 계층 구조를 갖는 객체를 개조할 수 있다.
BuildTree()
을 보면, GetChildren()
_과 _CreateGraphicNode()
을 이용하여 구현되고 있는데, 실제 수행 시에는 TreeDisplay
를 상속하는 DirectoryTreeDisplay
로 행동이 수행되어(가상 함수를 사용하여 자동으로 DirectoryTreeDisplay::GetChildren()
, DirectoryTreeDisplay::CreateGrahpicNode()
로 바인딩되어) FileSystemEntity
로 접근하여 자식 노드를 얻으면 그래픽 노드 처리가 가능해진다.
b. 위임 객체를 사용하는 방법
TreeDisplay
클래스가 자신에게 요청된 메세지를 다른 위임 객체에게 전달하는 방법이다. 이때, 서로 다른 위임 객체를 사용함으로써, 각기 다른 적응 전략을 구사할 수 있다.
예시
DirectoryBrowser
클래스가 TreeDisplay
클래스를 사용한다고 가정하자. DirectoryBrowser
클래스는 트리 구조 → 계층적 디렉토리 구조로 개조하는 위임자 역할을 수행한다. 이로 인해 서브클래스의 양을 줄일 수 있다.
C++같은 정적 타입 언어에서 위임 처리를 하려면 위임자를 맡은 명시적인 인터페이스가 필요하다. 이러한 인터페이스는 TreeDisplay
클래스가 요구하는 최소 인터페이스를 TreeAccessorDelegate
에 넣음으로써 정의할 수 있다. 즉, TreeAccessorDelegate
클래스에서는 위임받은 연산만 정의해야 하며, 따라서 이 클래스는 추상 클래스로 정의해야 한다. 그러고 나서 TreeAccessorDelegate
를 상속하여 우리가 실제로 위임할 대상(DirectoryBrower
)에다 이 인터페이스를 섞는다. DirectoryBrowser
가 기존의 부모 클래스를 가지고 있다면 다중 상속, 아니라면 단일 상속을 사용한다. 이렇게 하는 것이 앞에서 TreeDisplay
클래스의 새로운 서브클래스를 정의하고 각각의 연산을 복합하는 것(추상 연산을 이용한 방법)보다 쉽다.
c. 매개변수화된 적응자를 사용하는 방법
적응자를 매개변수화하여 대체 가능한 적응자를 만들 수 있다. (솔직히 잘 모르겠어서 이 이상의 정리를 못하겠다.)
reference
Structural: 1. Adapter Pattern · jeuxdeau/gof-design-patterns Wiki (github.com)
gof의 디자인 패턴