DesignPatternSmalltalkCompanion:Adapter

From 흡혈양파의 번역工房
Jump to navigation Jump to search

ADAPTER (DP 139)

의도

클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환한다. Adapter 패턴은 호환성이 없는 인터페이스 때문에 함께 사용할 수 없는 클래스를 개조하여 함께 작동하도록 해준다.

구조

Dpsc chapter04 Adapter 01v2.png

논의

Adapter 패턴은 비순응자(nonconformist) 객체를 클라이언트가 알고 있는 인터페이스에 일치시키는 데에 사용된다.[1] 클라이언트는 특정 프로토콜을 사용하는 협력 객체와 (Targets) 어떻게 의사소통하는지만 알고 있다. 우리에겐 Target 인터페이스에 일치하지 않는 기존 클래스가 있는데 그 기능성을 재사용하고자 한다. 그리고 이 클래스의 인스턴스를 T 로 가정할 때, 우리는 T 가 Target의 역할을 하길 원하는데 이 때 Adapter 패턴을 이용해 클라이언트와 T 사이에서 Target 메시지를 T 가 이해할 수 있는 메시지로 해석할 수 있다. 이번 시나리오에서 T 는 Adaptee라고 부르겠다. 이것은 클라이언트가 T 로 메시지를 전송하길 원할 경우 T 로 의미상 동등한 메시지를 전송하는 T 의 Adapter로 대신 메시지를 전송한다.

클래스 어댑터

스몰토크에서는 [디자인 패턴] 편에 설명된 Adapter 패턴의 클래스 버전을 구현할 수 없다. [디자인 패턴] 편에서는 두 개의 슈퍼클래스을 혼합한 행위를 가진 새로운 클래스를 생성하기 위해 다중상속을 이용하고 있다. 스몰토크에서는 다중상속을 지원하지 않기 때문에 어댑터라는 개념을 이용할 수 없다.

C++/스몰토크의 차이

페이지 DP 147에 실린 코드를 살펴보면 C++ Adapter가 그 Adaptee의 타입을 선언해야 함을 볼 수 있다. 그럴 경우 Adapter는 선언된 클래스의 객체 또는 그로 상속된 서브클래스의 객체만 조정할 수 있을 것이다. 스몰토크에서는 Adapter가 Adaptee로 전송한 메시지를 구현하는 구현하는 클래스라면 강한 타이핑(strong typing)이 없이 어떤 클래스든 그의 Adaptee가 될 수 있다.

구현

여기서는 Adapter의 변형을 몇 가지 구현해보고자 한다.

Tailored Adapter

특정 조정 시나리오에 맞춘 Adapter로 시작해보자. [디자인 패턴] (DP 139)에 실린 예를 보면, DrawingEditor는 Shape 계층구조에 있는 클래스의 인스턴스인 그래픽 객체의 집합체를 유지한다. DrawingEditor는 이러한 그림그리기 객체들과의 의사전달을 위해 Shape 프로토콜의 메시지를 사용한다. 하지만 텍스트의 경우 기존 TextView 클래스에서 이용할 수 있는 기능성을 사용하길 원한다. 또한 DrawingEditor이 Shape 객체와 상호작용하듯이 TextView들과도 상호작용할 수 있길 원하지만 TextView는 Shape 프로토콜을 따르지 않는다. 따라서 우리는 TextShape Adapter 클래스를 Shape의 서브클래스로 정의한다. TextShape에는 TextView 객체를 참조하고 Shape 프로토콜에서 메시지를 구현하는 인스턴스 변수가 있다; 각 메시지는 캡슐화된 TextView로 서로 다른 메시지를 단순히 전송하는 일을 한다. 그리고 난 후 DrawingEditor와 TextView 사이에 TextShape을 끼워 넣는다.

Dpsc chapter04 Adapter 02.png

TextShape은 Shape 기능성을 직접 구현하는 대신 해석자 역할을 하여 Shape 메시지들을 TextView Adaptee가 이해하는 메시지로 변환한다: DrawingEditor가 Shape 프로토콜에 일치하는 메시지를 TextShape로 보내면, TextShape는 모양은 서로 다르나 의미상 동일한 메시지를 TextView 인스턴스로 전달한다.

Adapter가 정의될 당시에 Adapter와 Adaptee의 인터페이스들은 이미 알려져 있다; 따라서 우리는 Adapter를 특정 case에 맞출 수 있으므로, 특정 Shape 메시지를 특정 TextView 메시지로 해석할 수 있다. 이것을 Tailored Adapter라고 부른다.

아래는 TextShape Adapter에 대한 상호작용 다이어그램으로, 해당 Adaptee를 위한 해석 메시지 중 하나를 보여준다:

Dpsc chapter04 Adapter 03.png

메시지 전달을 대체할 수 있는 어댑터

Message-Forwarding Pluggable Adapter

Tailored Adapter에서는 설계 시점에서 Adapter와 Adaptee의 프로토콜을 모두 알고 있었기 때문에 메시지를 해석하는 사용자정의 메소드를 (custom method) 쓸 수 있었다. Adapter 클래스는 고유의 해석 상황을 위해 부호화되었고, Adapter의 각 메소드는 Adaptee로 전달할 특정 메시지를 하드코딩하였다.

Adapter 시나리오의 두 번째 유형은 설계 시점에서 Adaptee의 인터페이스가 알려지지 않은 경우 발생한다. 선험적으로 Adaptee의 인터페이스가 알려지지 않았기 때문에 한 인터페이스에서 다른 인터페이스로 메시지를 무작정 해석할 수가 없다. 이러한 경우 대체할 수 있는(pluggable) 어댑터를 사용하여 일반적 방식으로 메시지를 변환 후 전달할 수 있다. Tailored Adapter와 마찬가지로 대체할 수 있는 어댑터도 클라이언트와 Adaptee 간 해석자 역할을 한다. 하지만 대체할 수 있는 어댑터의 경우 특정 경우마다 새로운 Adapter 클래스를 정의하지 않아도 된다. 대체할 수 있는 어댑터 사용의 동기를 부여할 수 있는 예제를 고려해보자.

대화형 애플리케이션용 MVC(모델-뷰-컨트롤러)에서는 뷰 객체들이 (화면의 위젯을 의미) 기본이 되는 애플리케이션 모델과 연결되어 있어서 모델에 변화가 생기면 사용자 인터페이스에 반영되고, 사용자가 인터페이스 시 변경 내용은 기본이 되는 모델 데이터를 변경하게 될 것이다. 뷰 객체들은 어떤 대화형 애플리케이션에서든 사용할 수 있도록 구현된다. 따라서 뷰 객체들은 자신의 모델과 의사소통하기 위해 일반 프로토콜을 사용한다; 특히 value로 전송되는 getter 메시지와 일반 setter 메시지는 value:가 된다. 예를 들어, 비주얼웍스의 TextEditorView가 내용을 얻는 방법은 다음과 같다:

TextEditorView>>getContents
	| t |
	t := model value.
	^t == nil
		ifTrue: [Text new]
		ifFalse: [t]

반면 애플리케이션 모델 객체들은 일반적으로 하나의 값보다는 다수의 측면을 가진다. 그들이 물론 단일 측면을 나타내긴 하지만 모델 객체들은 value나 value: 보다는 좀 더 의미 있는 이름을 가진 도메인 특정적 접근자 메시지를 사용한다. 그렇다면 문제는 모델이 이해하지 못하는 value와 같은 메시지를 뷰가 전송할 경우 뷰와 모델을 어떻게 연결시키느냐가 된다. 그에 대한 해결책으로 어떤 메시지를 value 로 해석하는지를 대체할 수 있는 어댑터에게 말해줄 수 있다ㅡ다시 말해, value 메시지를 수신할 때 그것의 Adaptee로 어떤 메시지를 전송할지를 말해준다. value:의 경우에서도 마찬가지이다.

구체적인 예를 들어보겠다. 직원관리 애플리케이션이 하나 있다고 치자. 애플리케이션 모델에는 직원의 사회 보장 번호를 나타내는 속성(여기서는 하나의 인스턴스 변수에 불과)이 포함되어 있고, 애플리케이션의 사용자 인터페이스에는 직원의 사회 보장 번호를 나타내는 입력상자 뷰가 포함되어 있다. 사회 보장 번호로 접근하고 이를 설정하는 모델의 메소드는 각각 socialSecurity와 socialSecurity:라는 이름을 가진다. 입력상자는 현재 사회 보장 번호를 표시해야 하지만 모델의 value를 요청하는 방법만 알고 있을 뿐이다. 따라서 우리는 value 메시지를 socialSecurity로 변환해야 한다. 이러한 목적으로 대체할 수 있는 어댑터를 사용할 수 있다. 이 예제에 맞는 상호작용 다이어그램은 다음과 같다:

Dpsc chapter04 Adapter 04.png

이 다이어그램은 단순화되어 있다; 대체할 수 있는 어댑터가 어떻게 작용하는지를 개념적으로 보여준다. 따라서 Adaptee로 전송되는 메시지는, 실제적으로 메시지의 상징적 재현과 간접 메시징을 허용하는 perform: 를 이용해 구현된다. 대체할 수 있는 어댑터는 메시지 선택기(message selector)를 Symbol로 저장하여 어느 때건 Adaptee에게 그 선택기를 일반 메시지의 전송처럼 실행하라고 말할 수 있다. 예를 들어, selector가 Symbol #socialSecurity를 참조할 때 직접 메시지 anObject socialSecurity는 anObject perform: selector와 동등하다. 이는 대체할 수 있는 어댑터의 메시지 전달 형태, 또는 메시지 기반의 대체가능한 어댑터를 구현하는 데에 있어 핵심이다. Adapter의 클라이언트는 대체할 수 있는 어댑터에게 value와value: 에 해당하는 메시지 선택기를 알려주고, Adapter는 이 선택기들을 내부로 저장한다. 상기 설명한 예를 들어보면, 클라이언트가 Symbol #socialSecurity를 value와 연관시키고, #socialSecurity: 는 value: 와 연관시킬 것을 Adapter에게 통보하는 것이다. 두 메시지 중 하나가 수신되면 Adapter는 메시지 선택기와 관련된 메시지를 perform: 을 이용해 Adaptee로 전달한다. 이것이 어떻게 작동하는지는 예제 코드에서 상세히 설명하겠다.

파라미터화된 어댑터

메시지 기반의 대체가능한 어댑터는 클라이언트로부터 일반 메시지 중 하나를 (value 또는 value:) 수신하면 그것의 Adaptee로 하나의 메시지를 전송한다. 클라이언트 메시지가 수신되었을 때 Adaptee에 행위를 호출하는 다른 방법들도 있다. 그 중 여기서는 블록스(blocks)를 살펴보겠다.

우리는 일반 Adapter 메시지가 각각 수신될 때 클라이언트가 메시지 선택기를 공급하는 대신 전체 코드 블록스가 실행하도록 공급할 수 있을 경우 Adapter를 가질 수 있다. 블록은 Adaptee의 호출 목적이라면 언제든 사용할 수 있지만 주로 Adaptee의 특정 측면에 접근하기 위한 목적으로 사용될 것이다. Adapter 클래스를 ParameterizedAdapter로 명명할 수 있다. 이것은 Adaptee를 기반으로 하나의 값을 검색하는데 사용되는 get block의 저장용 인스턴스 변수 하나와 Adaptee의 일부 측면을 설정하기 위한 set block용 인스턴스 변수 하나를 가질 것이다.

Object subclass: #ParameterizedAdapter
	instanceVariableNames: 'adaptee getBlock setBlock'
	classVariableNames: ''
	poolDictionaries: ''

ParameterizedAdapter class>>on: anObject
	^self new adaptee: anObject

모든 인스턴스 변수에 대한 setter 메시지를 가정할 때, 클라이언트는 ParameterizedAdapter를 생성하여 그 블록스를 다음과 같이 공급할 수 있다:

adapter := ParameterizedAdapter on: myGraphicalObject.
adapter
	getBlock: [:graphic | graphic boundingBox leftTop x];
	setBlock: [;graphic :newValue |
		graphic boundingBox leftTop x: newValue].

Adapter가 value 메시지를 수신하면 get block으로 명시된 값을 검색하기 위해 다음 메소드가 호출된다:

ParameterizedAdapter>>value
	^getBlock value: adaptee

우리 예제에서는 adapter value가 Adaptee의 경계상자(bounding box)에서 좌측상단 모서리에 위치한 x 좌표 값을 리턴한다.

값설정 블록(value-setting block)은 다음과 같이 사용된다. 블록의 첫 argument는 Adaptee에 묶여 있고 두 번째 argument는 클라이언트가 설정하는 새로운 값에 묶인다.

ParameterizedAdapter>>value: anObject
	setBlock value: adaptee value: anObject

따라서 클라이언트는 Adaptee의 경계상자의 원점을 변경하도록 adapter value: 10이라고 말할 수 있다.

[디자인 패턴] 편에서는 이러한 Adapter 타입을 파라미터화된 어댑터라고 부른다 (DP 145); 사실상 대체 가능한 어댑터의 다른 종류라고 보면 된다ㅡAdapter에게 value 또는 value:를 수신할 때마다 단일 메시지 선택기를 전달하는 대신 블록을 수행하라고 말할 뿐이다. 이는 블록 기반의 대체가능한 어댑터라고 부르면 되겠다. 사실 우리가 메시지 기반의 대체가능한 어댑터라고 부르는 것을 구현하는 클래스는 비주얼웍스의 AspectAdaptor에 해당하며, [디자인 패턴]에서 파라미터화된 어댑터라고 부르는 것은 PluggableAdaptor 클래스에 의해 구현된다. (알려진 스몰토크 사용예 참고)

대안적 해법

스몰토크로 Tailored Adapter를? Tailored Adapter의 사례에서와 같이 클라이언트가 Adaptee 메소드로 초기화한 메시지의 구체적인 매핑을 설계 시 알고 있다면 Adaptee 클래스에 이러한 해석 메소드를 직접 부호화하여 Adapter를 모두 함께 사용하는 것을 피하는 방법도 있지 않을까? Shape 예제로 다시 돌아가 우리가 만약 boudingBox를 getExtent로 해석해야 한다는 사실을 알고 있다면 이 해석을 TextView 클래스에 단순히 직접 부호화시키면 된다. 그러면 DrawingEditor가 Adapter 객체를 방해하지 않고 TextView 인스턴스로 직접 메시지를 전송한다.

C++의 경우, 클래스로 메소드를 추가하는 것은 전체 클래스를 다시 컴파일해야 함을 의미하므로 이 접근법을 사용하고 싶지 않을 뿐 아니라, 클래스에 대한 소스 코드가 없다면 이러한 접근법을 사용하고 싶어도 할 수 없다. 스몰토크에서는 각 메소드의 증분 컴파일(incremental compilation)이 활성화된 경우 매우 간단히 사용할 수 있다. 다음과 같이 단순히 부호화시키기만 하면 된다:

TextView>>boundingBox
	"Translate the message and delegate to me"
	^self getExtent

그 결과 당신은 스몰토크에서 Tailored Adapter의 업무만 하는 객체를 찾느라 애를 먹을 것이다.

Tailored Adapter 서브클래스. 우리는 사용자 인터페이스 위젯 클래스와 같이 새로운 애플리케이션이 사용할 때마다 매번 애플리케이션 특정적 해석 메시지를 갖는 일반적으로 사용 가능한 클래스는 원하지 않는다. 여기에 사용할 수 있는 또 다른 대안책이 있다. Shape 계층구조에 새로운 Adapter 클래스를 정의하는 대신 동일한 종류의 해석 메소드로 TextView의 서브클래스를 구현하는 방법이 그것이다:

TextView subclass: #TextViewShape
	...

TextViewShape가 TextView 메소드를 모두 상속하도록 하여 서브클래스 내에 해석 메소드를 추가하도록 둔다. 이러한 메소드는 이전 코드와 같은 모습일 것이다:

TextViewShape>>boundingBox
	"Translate the message and delegate to me"
	^self getExtent

프로그램의 이해와 관리 측면에서 보면 수 많은 도메인 특정적 또는 애플리케이션 특정적 메시지로 유일한 클래스를 로드업(load up)하기란 약간 혼란스러울지 모른다. 반면 추가 매개 Adapter 객체로 부호화하는 것이 더 어려울 수도 있다. 하지만 앞서 정의한 TextShape 클래스는 Shape 계층구조에 위치하고 추상 프로토콜 Shape를 구현한다; 이는 클래스 계층구조와 접촉하지 않은 가지에 위치한 클래스로 하여금 동일한 프로토콜을 구현시키는 것보다 더 설득력 있고 일관성 있는 설계이다. 또한 프로토콜이 변경된 적이 있는 경우 여러 개의 구분된 위치가 아니라 계층구조 하나의 가지에 수정이 발생하므로 변경과 확장이 용이하다.

어댑터 역할. Adapter들의 사용은 비주얼웍스 사용자 인터페이스 프레임워크의 깊은 부분에 해당하기 때문에 비주얼웍스의 기본 이미지에는 수 많은 타입의 대체가능한 어댑터가 있다. 하지만 스몰토크 환경에 따라 매우 다른 사용자 인터페이스 프레임워크를 가진다. 일부 방언에서는 Adapter의 역할만 하는 객체를 찾기가 힘들지도 모르겠다.

비주얼 스몰토크를 예로 들자면, 윈도우 내 창들이 위젯 행위뿐 아니라 대체가능한 어댑터의 역할을 맡는다. 모든 윈도우 창은ㅡTopPane과 그 서브컬래스 또는 SubPane 서브클래스ㅡ다수의 사용자 상호작용 이벤트를 처리할 수 있다. 각 윈도우 창은 각 이벤트가 발생할 때마다 어떤 메시지를 누구에게 전송해야 할지 정보를 받는다. 대체가능한 메커니즘이 없다면 이러한 윈도우 창들은 각 이벤트에 대한 일반 메시지만 전송할 것이다. 따라서 그들은 일반적인 사용자 실행이 발생 시 애플리케이션이 이해하는 특정 메시지를 전송할 수 있도록 구성한다는 점에서 Adapter의 역할을 한다.

예를 들어, 윈도우 창이 내용을 채울 필요가 있을 경우 다른 객체로 (주로 애플리케이션 객체) 메시지를 전달하도록 설정해놓을 수 있다. 이는 비주얼웍스 위젯이 그 모델로 value 메시지를 보내 (애플리케이션 모델의 측면에서 보면 어느 정도 Adapter에 가까울지도 모른다) 위젯의 내용 또는 상태가 설정되는 상황과 거의 동일하다고 볼 수 있다. 이와 비슷한 방식으로, 윈도우 창은 사용자의 선택에 의해 윈도우 창이 변경 시 다른 객체로 특정 메시지를 전송하라는 통지를 받을 수 있다. 이는 비주얼웍스 위젯이 그 측면들 중 하나에 해당하는 새로운 값을 애플리케이션에게 통보하기 위해 (Adapter를 통해 가능) value: 메시지를 전송하는 비주얼웍스 위젯과 같다.

애플리케이션, 그 중에서 주로 ViewManager 서브클래스가 윈도우를 구성할 때면 이것은 각 윈도우 창에 대해 이벤트 메시지 관계를 설정한다. 아래에 MethodBrowser 클래스로부터 윈도우 빌딩 코드를 일부 소개하겠다:

MethodBrowser>>createView
	| pane |
	...
	self addSubpane:
		((pane := ListPane new)
			when: #needsContents send: #methodList:
				to: self with: pane;
			when: #needsMenu send: #listMenu:
				to: self with: pane;
			when: #clicked: send: #method:
				to: self with: pane;
			...)

ListPane이 그 내용(목록)을 필요로 하는 경우 그것은 MethodBrowser로 methodList: 를 전달하고 스스로를 메시지의 argument로 넘길 것이다. 사용자가 그 목록에서 엔트리를 클릭하면 다시 ListPane을 argument로 하여 method: 가 전송되어 MethodBrowser가 윈도우에 최근 선택사항을 요청한다. 이런 방식으로 윈도우는 스스로 일반 이벤트를 애플리케이션 특정적 메시지로 조정한다. (비주얼 스몰토크의 이벤트 머시너리(event machinery)는 Command (245)와 Observer 패턴에서도 자세히 다루고 있다 [비주얼 스몰토크의 SASE 참조, P. 314]).

IBM 스몰토크에서는 이와 비슷한 접근법을 채택하여 위젯이 이벤트를 애플리케이션 특정적 메시지로 조정한다는 점에서 스스로의 대화형 위젯 행위뿐 아니라 Adapter의 역할도 충족시킨다고 할 수 있다. 애플리케이션 객체는 callback이나 event handler 메커니즘을 사용하여 사용자 실행이 발생 시 이 정보를 통보받도록 등록할 수 있다 (IBM 스몰토크는 사용자 실행을 두 가지 추상화 수준으로 구분한다: 낮은 수준의 마우스 및 키 누름 오퍼레이션은 이벤트 핸들러 머시너리에 의해 처리되고, 버튼 위젯에서 윈도우를 닫거나 완료 버튼다운/버튼업 실행과 같은 높은 수준의 실행은 callback에 의해 처리된다). 두 메커니즘 모두 각 위젯 이벤트마다 전송해야 할 메시지를 애플리케이션이 명시하도록 한다. (IBM 스몰토크의 callback과 이벤트 처리기 메커니즘에 관한 상세 내용은 IBM 스몰토크의 317페이지 SASE에 설명된 Observer 패턴과 Smith, 1995, Shafer & Herndon, 1995를 참조하라).

비주얼웍스에서조차 특정 뷰는 다수의 어댑터 클래스로 어댑터의 역할을 충족할 수 있다. 일부 View 클래스는 대체가능하도록 구현되어, 뷰가 "관찰하는" 측면 뿐만 아니라 특정 이벤트를 위한 애플리케이션 특정적 메시지를 애플리케이션이 명시하도록 만들 수 있다. 예를 들어, TextView 클래스 코멘트 설명은 다음과 같이 나타낼 수 있다: "TextView는 ‘대체가능한’ 뷰이다…최고의 메커니즘은 일반 TextView 오퍼레이션을 모델특정적 오퍼레이션으로 변환하는 어댑터로 간주할 수 있는 선택기 집합체이다." 다음 메시지로 TextView들을 생성할 수 있다:

newTextView := TextView
		on: self
		aspect: partMsg
		change: acceptMsg
		menu: menuMsg
		initialSelection: selection

예를 들자면, partMsg argument는 그 텍스트 측면을 공급하기 위해 애플리케이션이 구현하는 메시지인 #text 값을 가질 수도 있다. TextView에 그 내용이 필요한 경우 그것은 다음 애플리케이션에서 자체 제작된 메시지를 이용해 그 모델로부터 내용을 얻는다:

TextView>>getContents
	| text |
	partMsg == nil ifTrue: [^Text new].
	text := model perform: partMsg.
	text == nil ifTrue: [^Text new].
	^ text

예제 코드

파라미터화된 어댑터를 (또는 블록 기반의 대체가능한 어댑터) 구현하는 대부분의 코드를 살펴보았다. 이제 Tailored Adapter와 메시지 기반의 대체가능한 어댑터를 구현하는 방법을 보이고자 한다.

Tailored Adapter

[디자인 패턴] 편의 기본 어댑터를 스몰토크에서 구현하는 것을 보고 싶어하는 독자들을 위해 Tailored Adapter에 관한 예제 코드로 시작하고자 한다. 다음 코드는 TextShape 어댑터를 구현한다. TextShape 클래스는 Shape 계층구조에 상주하므로 Shape 추상 인터페이스를 구현한다. 이 클래스는 TextView 어댑터를 참조하게 될 인스턴스 변수를 정의한다:

Shape subclass: #TextShape
	instanceVariableNames: 'textView'
	classVariableNames: ''
	poolDictionaries: ''

TextShape class>>new
	"Return a new instance of me pointing
	to an instance of TextView."
	^self basicNew textView: TextView new

다음으로 간단한 getter 메소드와 setter 메소드를 정의한다:

TextShape>>textView
	"Return my Adaptee"
	^textView

TextShape>>textView: aTextView
	"Set my Adaptee"
	textView := aTextView

…실제 해석 메소드는 다음과 같다:

TextShape>>boundingBox
	"Translate and delegate this to my TextView object."
	^self textView getExtent

[디자인 패턴] (DP 147)편에서 언급한 바와 같이 어댑터들은 메시지를 해석하지 않고 어댑티로 전송한다. 이는 어댑티 프로토콜의 일부와 어댑터 프로토콜의 일부가 일치하는 경우 발생한다.

구체적으로 말하자면, 메시지가 어댑터와 어댑티에서 이름과 의미가 동일한 선택기를 가지는 경우 직접 전달될 수 있음을 의미한다. 이는 단순한 위임에 불과하다; 어댑터가 어댑티에게 응답의 책임성을 위임하는 것이다. 다음 isEmpty 구현을 예로 들어보겠다:

TextShape>>isEmpty
	^self textView isEmpty

그럼에도 불구하고 어댑터는 최소한 Target 프로토콜의 일부라도 해석해야만 한다; 그렇지 않을 시 어댑터가 쓸모없게 된다.

메시지 기반의 대체가능한 어댑터

Message-Based Pluggable Adapter

메시지 전달을 대체할 수 있는 어댑터는 주로 value와 value: 처럼 한 쌍의 getter-setter 일반 메시지를 구현한다. 클라이언트는 어댑터에게 이 메시지를 어떻게 해석해야 하는지 말해주어야 한다; 즉 클라이언트는 value와 value: 를 대신하여 전달될 메시지 선택기를 어댑티로 공급한다. 이러한 해석은 상징적으로 표현된다: 클라이언트는 선택기를 Symbol로 명시하고, 어댑터는 이 양식으로 저장한다. 아래 코드를 살펴보면 더 명확하게 이해될 것이다.

어댑터의 클래스 정의와 어댑터에 대한 getter-setter 메소드로 시작하겠다:

Object subclass: #MessageAdapter
	instanceVariableNames: 'adaptee getSelector setSelector'
	classVariableNames: ''
	poolDictionaries: ''

MessageAdapter class>>on: anAdaptee
	"Instance creation"
	^self new adaptee: anAdaptee

MessageAdapter>>adaptee: anObject
	adaptee := anObject

MessageAdapter>>adaptee
	^adaptee

다음은 클라이언트가 어댑터에게 메시지를 해석하는 방식을 알려주기 위해 사용하는 메소드이다:

MessageAdapter>>getSelector: aSymbol
	"Setup my getter message translation.
	aSymbol is the selector to send to my Adaptee
	when I receive the #value message"
	getSelector:= aSymbol

MessageAdapter>>setSelector: aSymbol
	"Setup my seeter message translation.
	aSymbol is the selector to send to my Adaptee
	when I receive the #value: message"
	setSelector:= aSymbol

MessageAdapter>>onAspect: aspectSymbol
	"A handy method to set both setter and getter
	messages in one shot; assumes both have the same name,
	differing only by the ':' suffix for the setter."
	self
		getSelector: aspectSymbol;
		setSelector: (aspectSymbol, ':') asSymbol

어댑터가 일반적 getter (value) 또는 setter (value:) 메시지를 수신하고 나면 어댑티로 해석된 해당 메시지를 전송함으로써 응답한다:

MessageAdapter>>value
	"Return the aspect of my Adaptee specified by
	my getSelector"
	^adaptee perform: getSelector

MessageAdapter>>value: anObject
	"Set the aspect of my Adaptee specified by
	my setSelector"
	^adaptee perform: setSelector with: anObject

다음과 같이 코드에 의해 MessageAdapter가 인스턴스화 및 초기화된다:

adapter := MessageAdapter on: myApplicationModel.
adapter
	getSelector: #socialSecurity;
	setSelector: #socialSecurity:.

위의 코드 대신 뒷부분 3줄을 다음으로 교체하여 간단하게 나타낼 수도 있다:

adapter onAspect: #socialSecurity.

알려진 스몰토크 사용예

AspectAdapter

메시지 기반의 대체가능한 어댑터로 구현한 것이ㅡMessageAdapter 클래스ㅡ비주얼웍스의 AspectAdaptor 클래스와 거의 동일하다고 볼 수 있다. AspectAdaptor는 getter 와 setter 메시지 선택기를 설정하기 위해 accessWith: getSymbol assignWith: putSymbol 메시지를 전송할 수 있다.

경로 기반의 어댑터

Path-Based Adapter

AspectAdapter의 슈퍼클래스인 ProtocolAdaptor 또한 Adapter 주제에 한 가지 변형을 구현한다. 단일 getter (또는 setter) 메시지로 검색할 수 없는 (또는 설정할 수 없는) 어댑티의 특정 측면에 관심을 가질 수도 있겠다. ParameterizedAdapter 예제에서와 같이 어댑티가 그래픽 객체라고 가정하자. 이번에도 마찬가지로 value 메시지가 수신될 때 어댑터가 어댑티의 구체적 측면ㅡ특히 그래픽의 경계상자 원점의 x 좌표ㅡ을 검색하길 원한다. 유일한 선택기 또는 블록을 value 메시지와 관련시키는 대신 ProtocolAdaptor의 구체적 서브클래스는 AspectAdaptor 예제와 같이 클라이언트로 하여금 어댑티로 전송할 선택기 목록을 명시하도록 허용한다:

adaptor := AspectAdaptor new.
adaptor
	subject: myGraphicalObject;
	accessPath: #(boundingBox origin x).

subject: 메시지에 대한 argument는 어댑티가 된다. AspectAdaptor가 value 메시지를 수신하면 잇따라 어댑티의 구체적 측면을 검색하기 위해 접근 경로열(access path array)로 메시지를 전송한다. 우리는 이를 경로 기반의 어댑터라고 라벨을 붙일 수 있다. 이런 경우 어댑티는 그래픽 객체이다. 먼저 boundingBox 메시지가 객체로 전송되고 나면 Rectangle을 반환한다; 그리고 난 후 origin 메시지는 Point를 반환하는 Rectangle로 전송된다; 마지막으로 x가 Point로 전송되고 x 좌표를 검색한다. 따라서 그래픽의 경계상자가 Rectangle이고 그 상단좌측이 1@2인 경우, 다음 문은 1을 리턴시킨다:

adaptor value

흥미롭게도 AspectAdaptor는 여러 접근법을 섞어 함께 사용할 수 있도록 해준다: getter와 setter 메시지만 명시할 수도 있고, 접근 경로만 명시할 수도 있으며, 두 가지 모두 명시할 수도 있다. 따라서 상기 코드는 다음 코드와 동등하다:

adaptor := AspectAdaptor new.
adaptor
	subject: myGraphicalObject;
	accessPath: #(boundingBox origin) ;
	accessWith: #x assignWith: #x:.

adaptor value "returns 1"

이 예제는 경계상자의 상단 좌측 모서리에 x 좌표를 설정할 수 있게 해준다. 다음 문은 10@2로 원점을 변경시킨다:

adaptor value: 10.

adaptor value "now returns 10"

제약 기반(constraint-based)의 그래픽 편집기 2개에서 그래픽 객체의 구체적 측면으로의 일반적 접근성을 제공하기 위해 위와 동일한 경로 기반의 조정을 사용하였다. 예를 들어, 제약 기반의 편집기에서 라인을 수평으로 제약하기 위해선 이것이 필요하다. 여기서는 제약 객체가 라인 끝 지점의 y 좌표로 접근해야 한다. 이를 위해선 두 개의 경로 기반 어댑터를 이용할 수 있다; 두 가지 전부 라인 객체가 어댑티가 될 것이며 각각의 접근 경로, # (endpoint1 y)과 # (endPoint2 y)를 필요로 한다. 이 상황을 처리하기 위해 ThingLab (Borning, 1981)은 경로 기반의 어댑터 메커니즘을 구현하였고, 이로 인해 ProtocolAdaptor에서와 마찬가지로 그래픽 객체의 값을 검색하는 일반 메소드로 구체적 측면을 검색할 수 있었다 (사실상 빈 접근 경로의 경우 전체 그래픽 객체가 검색될 것이다); Grace (Alpert, 1993)도 이와 동일한 기본 기법을 조정하였다. 두 사례 모두 기존의 ProtocolAdaptor와 같은 클래스가 없는 환경에서 경로 기반의 어댑터가 구현된 사례이다. 그리고 두 사례 모두 대상(어댑티)의 구체적 측면을 검색하기 위해 접근 경로 열을 사용하는 코드는 기본적으로 다음과 같은 모습이다:

^accessPath
	inject: subject
	into: [:obj :msg | obj perform: msg]

PluggableAdaptor

비주얼웍스는 PluggableAdaptor라는 이름의 클래스도 포함하고 있다. 여기서 클라이언트는 접근자 선택기의 상징적 이름을 공급하는 대신 value 또는 value: 메시지가 수신될 때 실행될 코드의 블록을 명시한다.

여기서 짚고 넘어가야 할 부분은 둘의 차이점이다. 선택기 접근법의 경우 최종 행위가 어댑티에서 구현된다; 어댑터는 그 행위를 호출하기 위해 어떠한 메시지를 전송해야 하는지만 알고 있다. 반면 블록을 통해 조정할 행위를 공급하는 것은 실행시킬 코드가 (인스턴스 변수의) 어댑터 안에 위치하여 사실상 어댑티의 정의와 독립되도록 만든다는 것을 의미한다. 블록은 더 많은 유연성을 제공한다; 불편한 점을 들자면 코드를 이해하기가 까다롭다는 점이다.

비주얼웍스 사용자 인터페이스 프레임워크

비주얼웍스 UIPainter를 이용해 구성된 모든 대화형 애플리케이션은 사용자 인터페이스 윈도우와 위젯을 구축하기 위해 런타임 시 UIBuilder 프레임워크를 사용한다. 이 프레임워크를 사용하는 모든 애플리케이션은 사용자 인터페이스 위젯과 기본이 되는 애플리케이션 모델 간의 어댑터들을 사용한다.

비주얼웍스 이미지의 추가 예제

비주얼웍스 기반 클래스 라이브러리에는 더 많은 어댑터 변형이 사용되고 있으며, 이는 모두 ValueModel 계층구조에서 클래스에 의해 구현된다 (하지만 다음 단락을 주목). Woolf (1995b)는 ValueModel들에 대한 면밀한 논의를 제공한다; Lewis (1995)와 Howard (1995)이 비주얼웍스 어댑터와 관련해 작성한 글도 참고하길 바란다.

오리처럼 생기긴 했는데 오리처럼 꽥꽥거릴까?

ValueModel은 어댑터처럼 보이는 서브클래스도 포함하고 있지만ㅡValueModel 계층구조에 있으며 value와 value: 메시지를 구현한다ㅡGoF 기준에 따르면 이 서브클래스들은 어댑터가 아니다. 실제 어댑터라면 어댑티는 어댑터의 평생 동안 영속적 하위컴포넌트가 되고, 어댑터가 메시지를 수신하면 그것의 어댑티로 두 번째 (주로 다른) 메시지를 전달한다. 반면 ValueModel 서브클래스인 ValueHolder의 인스턴스는 단순히 다른 객체를 둘러쌀 뿐이다. 래퍼 객체들 중 하나가 value 메시지를 수신하면 이는 둘러싼 객체를 반환하는 일만 한다; 메시지를 전송하는 일은 하지 않는다. 더 중요한 점은, "진정한" 어댑터는 value: 메시지를 받으면 두 번째 메시지가 어댑티로 전달된다는 점이다. 메시지가 어댑티를 수정할지도 모르지만 캡슐화된 어댑티 객체는 똑같은 객체로 남아 있는다. ValueHolder 래퍼가 value: 메시지를 수신하면 그것은 임베디드 객체를 대체한다; 따라서 이제 다른 객체가 되는 것이다. 비주얼 스몰토크의 SharedValue 클래스는 이와 같은 방식으로 행동한다.

이러한 구현은 GoF의 어댑터는 확실히 아니지만 개발자들이 특정 애플리케이션 컨텍스트에서 꽤 유용한 것으로 밝힌 몇 가지 조정하는 것들이 있다. 중요한 점은, 때로는 우리가 패턴과 유사한 무언가를 구현할 수 있고ㅡ패턴 정의의 실제 예제라고 볼 수는 없지만ㅡ여전히 유용하게 사용될 수 있다는 점이다. 어댑터와 같이 ValueHolder와 ShareValue 들은 객체를 둘러싼 후 래퍼가 일반 value 메시지에 응답하길 원하는 조정 상황에 유용하다; 물론 GoF 어댑터와는 다른 방식이겠지만 그럼에도 불구하고 객체 값을 검색하기 위해 value 메시지를 전송하는 법만 알고 있는 객체들이나 위젯의 경우 유용하다. 래퍼들은 (SharedValue 또는 ValueHolder) 스몰토크에서는 허용되지 않는 실제 참조에 의한 호출(call-by-reference) 의미론을 시뮬레이션하는데 사용되기도 한다. (스몰토크에서 메소드는 argument로 메시지를 전송할 수는 있으나 할당 연산자 (:=)를 이용해 argument에 새로운 객체를 할당할 수는 없다; 그러나 argument가 래퍼인 경우 메소드는 argument에게 value: 메시지를 전송할 수 있고ㅡ둘러싼 객체를 교체ㅡ전송자는 이에 따라 래퍼에게 value 를 보냄으로써 새로운 객체를 검색할 수 있다.) 결말은 패턴을 정확히 있는 그대로 적용할 수도 있고 약간 다른 애플리케이션 컨텍스트의 경우 패턴의 표준 정의를 본뜨거나 조정할 수도 있다는 점이다.

관련 패턴

Observer

가끔씩 어댑터는 Observer (305) 패턴에 참여하기도 한다. 특정 어댑터에게 구체적인 객체 또는 종속 객체 집합에 변경이 일어날 경우 그것으로 메시지 전송을 트리거하라고 말할 수 있다. 비주얼웍스를 예로 들면, 어댑터는 ValueModel 계층구조에 위치하고 ValueModel은 onChangeSend:to: 메시지를 구현한다. 어댑터가 변경하라는 소식을 들으면 이를 이용해 구체적 객체에게 알려준다.

adapter onChangeSend: #changeOccurred to: self

adapter가 value: 메시지를 수신하고 나면 이는 어댑티가 값을 설정한 일반 행위에 더해 changeOccured 메시지를 전송할 것이다.

Command

어댑터는 Command (245) 패턴에서 Command 객체로 사용되기도 한다. 사실 두 객체는 비슷한 역할을 수행한다; 둘 다 클라이언트와 어댑티 사이에 위치하고, 두 패턴 모두 클라이언트가 어댑티로 전달하기 위해 제3자에게 보낸 메시지를 해석한다. Command는 클라이언트가 사용자 인터페이스 위젯이거나 컨트롤인 경우와 같이 (예: 버튼 또는 메뉴 항목) 전문화된 역할만을 가지며, 어댑티로 전송된 메시지는 컨트롤의 활성화에 대한 응답으로 실행되는 조치이다 (예: 버튼 누르기, 메뉴 항목 선택하기).

Bridge와 Façade

적어도 클라이언트의 관점에서 보면 어댑터는 어댑티의 인터페이스를 변경시킨다. 다른 패턴들 또한 클라이언트로 서로 다른 메시징 인터페이스를 표시하는 데 사용된다. Bridge (121) 패턴에서는, Abstraction 객체가 실제 작업이 이루어지는 구현 객체를 향해 메시지를 성공적으로 해석하여 전달한다. 그러나 Bridge와 어댑터는 의도나 적용성에 있어 서로 다르다. 어댑터는, 특정 프로토콜을 이용해 자신의 협력자와 의사소통하는 방법만 알고 있는 객체를 갖고 있지만 그 프로토콜을 이해하지 못하는 객체와 협력할 필요가 있는 경우에 적용할 수 있다. Bridge는 "인터페이스와 구현을 구분하여 쉽고 독립적으로 변화할 수 있기 위함"이 목적이다 (DP 149). 기존 객체를 양립할 수 없는(incompatible) 프로토콜로 개조하기 위해 만들어진 것이 아니다.

Façade (179)는 객체의 모든 하위시스템에 추상 인터페이스를 제공하는 반면 어댑터는 유일한 객체를 클라이언트에 조절한다. 물론 두 패턴의 구현과 적용성은 꽤 다르지만 그 정신, 다시 말해 클라이언트에게 대안적 인터페이스를 제공한다는 점은 같다.

Decorator

Decorator (161) 또한 클라이언트와 다른 객체 (컴포넌트) 간 위치하여 클라이언트 메시지를 전송하고 컴포넌트 일부 또는 전부에게 메시지를 전달한다. 그러나 Decorator는 컴포넌트의 인터페이스를 완전히 따르면서 기능을 향상시키는데 사용된다. 어댑터와 달리 Decorator는 그것이 장식하는 컴포넌트의 인터페이스를 조정하지 않는다.

Notes

  1. Adapter와 Adaptor 둘 다 사용된다. 여기서는 [디자인 패턴]과 동일하게 전자를 사용하고자 한다. 단 VisualWorks Adapters를 논할 때에는 Adaptor를 사용한다.