TheArtandScienceofSmalltalk:Chapter 10
- 제 10 장 pluggability 와 어댑터
pluggability 와 어댑터
객체 지향 프로그래밍의 주요 혜택 중 하나는 재사용성이다. 다른 사람의 코드를 이용해 (특히 클래스 라이브러리에서) 자신만의 재사용 가능한 클래스를 작성할 수 있는 기능이야말로 OOP를 재래식 프로그래밍보다 더 생산적으로 만들어주는 것이다. 그러한 방법은 뻔해 보이지만 사실 기존 코드를 재사용하는 방법에는 여러 가지가 있다.
OOP를 할 때 우리가 생각하는 첫 번째 재사용 형태는 바로 상속이다. 상속은 기존 클래스와 어떻게 다른지 명시함으로써 간단히 새 클래스를 정의하도록 해준다. 그러한 차이는 전체적인 기능보다 훨씬 작기 때문에 엄청난 효율성을 얻을 수 있다.
상속보단 덜 명백하지만 스몰토크에서 자주 사용되는 재사용 형태는 기존 클래스의 인스턴스를 만들어 상속 없이 사용하는 방법이다. 숫자, 문자열, 컬렉션, 위젯 등 라이브러리 내 나머지 클래스를 사용할 때가 이에 해당한다.
스몰토크를 소프트웨어 구성자 키트(software constructor kit)처럼 취급하여, 실제로 자신만의 클래스를 작성하지 않고 기존 클래스의 인스턴스를 서로에게 '이식(plug)'하는 것이 세 번째 재사용 형태다. 이는 VisualWorks GUI 툴이 매우 효과적으로 의존하는 기법이다.
마지막 두 개의 재사용 형태는 문제를 발생시킬 수 있다. 가령 컬렉션 클래스의 인스턴스를 만들 때 당신은 (프로그래머는) 그들이 지원하는 프로토콜을 (메시지 집합) 알고 있으며, 자신의 코드를 적절하게 작성할 수 있다. 하지만 UI 위젯 클래스의 작성자는 당신의 클래스가 어떤 프로토콜을 이해하는지 알지 못하기 때문에 위젯이 해당 클래스들과 작업하도록 특수화시킬 수가 없다. 이와 비슷하게, 스몰토크를 구성자 키트로 취급한다는 것은 모든 구성요소(component)가 서로 이식(plug) 가능해야 하고, 그에 따라 클래스가 작성될 때 예상치 못한 방식으로 구성 가능해야 함을 의미한다.
다행히 스몰토크에는 프로그래머가 이러한 문제를 해결하는 데 필요로 하는 유연성으로 구축하도록 해주는 메커니즘이 몇 가지 있다 (블록과 performs 메서드). 클래스 라이브러리 내에는 이를 이용한 다수의 클래스가 있는데, 그 중 몇 가지만 이번 장에서 집중적으로 살펴보고자 한다.
pluggability 라는 개념과 그것을 지원하는 클래스를 언급해야 하는 데는 몇 가지 이유가 있다. 첫째, 이들은 VisualWorks UI 툴의 기반을 형성한다. VisualWorks 는 관련된 복잡성으로부터 당신을 숨겨주는 데 일가견이 있긴 하지만, 당신의 코드가 예상대로 작동하지 않을 때는 당신이 뚜껑을 열어 그 이유를 알아내야 하는 경우가 있다! 둘째, 관련된 프로그래밍 기법들은 가장 강력한 스몰토크의 기능을 몇 가지 보여준다-이 기능들은 자신만의 재사용 가능한 클래스를 작성하고자 할 때 귀중하다는 사실을 발견할 것이다.
이번 장의 메시지를 이해하기 위해서는 pluggability 의 기반이 되는 메커니즘, 블록과 perform: 을 이해할 필요가 있다. 블록은 제 4장, 스몰토크 언어에서 살펴보았으므로 여기서는 perform: 을 소개하겠다.
perform: 메커니즘
제 4장에서는 객체로 메시지를 전송하기 위한 구문을 살펴보았다. 객체로 메시지를 전송하기 위해서는 객체를 명명하고, 그것으로 전송하고자 하는 메시지를 입력하기만 하면 된다. 예를 들어:
MyObject reset.
이 코드가 시스템에서 실행되면 MyObject의 클래스를 검색하여 해당 클래스에 대해 적절한 reset 메서드의 구현부를 호출하라. 메서드의 이러한 런타임 검색이 다형성-객체마다 동일한 메시지에 대해 다른 방식으로 응답하는-을 야기한다는 사실을 기억할 것이다. 이는 객체 지향 프로그래밍의 매우 강력한 기능이지만 스몰토크에서는 이러한 유연성만으로 충분한 경우가 종종 있다.
위의 예제에서 사용될 reset의 구현부는 런타임 때까진 알려지지 않지만 메서드명은 컴파일시에 (브라우저에서 accept를 이용해 메서드가 저장될 때) 알려진다. 하지만 때로는 메서드명이 런타임 시에만 결정되기도 한다. 이러한 경우 해당 스몰토크는 perform: 라 불리는 메서드와 그 derivatives 로 이루어진 집합을 제공한다. 이러한 메서드들은 객체에게 위와 같이 하드코딩된 것이 아니라 매개변수로서 이름이 전달된 메서드를 실행하도록 허용한다. 즉, 메서드명은 런타임시까지는 알려질 필요가 없다는 의미다. 예를 들어:
MyObject perform: MyCommand.
이 표현식이 평가될 때는 MyCommand 변수에 포함된 메시지가 무엇이든 MyObject로 전송될 것이다. 이것은 MyObject가 이해하는 메서드를 명명하는 기호일 것으로 예상된다. 따라서 MyCommand가 ftrest 값을 가졌다면 reset 메시지가 MyObject로 전송될 것이다. 메서드명은 문자열이 아니라 기호여야 함을 주목한다.
매개변수와 함께 메시지를 객체로 전송해야 하는 경우 1, 2, 3 또는 매개변수 전체 배열을 전송할 수 있는 perform: 메시지의 변형자(variations)가 있다. 이러한 변형자는 다음과 같다:
perform:with:
perform:with:with:
perform:with:with:with:
perform:withArguments:
이러한 메커니즘은 이상해보일 뿐더러 필요한 경우가 극히 드물고, 일반 메시지 전송보다 덜 효과적이고 혼란만 더하기 때문에 주의하여 사용해야 한다. 하지만 재사용 가능한 코드를 작성할 때가 있는데 그 때는 perform: 이 강력한 기능이 된다. 블록의 개념과 함께 perform: 개념은 이번 장의 주제가 되는 pluggability 의 기반이 된다.
pluggability
바라건대 드디어 함수에게 (코드 블록에 value 메시지를 전송해 실행되는) 코드 블록 또는 (perform:을 이용해 다른 객체로 메시지를 전송하는) 기호를 제공하여 맞춤설정이 가능하고 일반 함수를 실행하는 클래스를 작성하는 것이 어떻게 가능한지 살펴볼 수 있겠다. 블록 또는 기호는 클래스의 인스턴스 변수에 보관된다. 고정된(fixed) 메시지 value 와 perform: 만이 원래(original) 프로그래머에 의해 하드코딩되고 기호 또는 블록은 런타임 시 재사용자가 제공한 것이어야 한다. 이는 사전 정의된 클래스의 행위를 서브클래싱할 필요 없이 인스턴스별(클래스별이 아니라) 기반으로 수정하도록 해준다.
클래스 라이브러리에는 이러한 pluggability 가 허용하는 '간접성(indirection)'을 사용하는 클래스가 몇 개 있다. 우선 세 가지를 살펴보겠다-그 중 하나는 위의 메커니즘 중 하나도 사용하지 않고 간접성을 제공하고 (ValueHolder), 하나는 perform:을 사용하며(AspectAdaptor), 나머지 하나는 블록을 이용한다(PluggableAdaptor). 뒷면에 실린 다이어그램은 이러한 클래스들에 들어맞는 계층구조 단편을 보여준다. 유사한 클래스가 많이 있음을 볼 수 있는데 (그 중 일부는 명료성을 위해 다이어그램에서 제거됨), 필요하다면 해당 클래스를 마음껏 살펴보고 사용해도 좋다. 이 모든 클래스들은 ValueModel 로부터 상속된다. 계층구조에서 해당 단편은 상속이 허용하는 추상화 유형을-즉 재사용 가능성이 매우 높지만 때로는 이해하기 힘든 클래스 정의를 생성하는-훌륭하게 보여주는 예가 되기도 한다.
이 모든 클래스의 인스턴스들의 일반적인 목적은 두 가지 다른 객체 간 커넥터 또는 어댑터 역할을 수행하는 것이다. 아래 다이어그램은 이것이 어떻게 작용하는지를 보여준다. 어댑터는 각 객체가 전송한 메시지 혹은 프로토콜을 다른 객체들이 이해하는 메시지로 해석한다. 이 덕분에 앞의 그림을 예로 들자면, 객체 A와 객체 B는 함께 작업하도록 빌드할 필요 없이 작업할 수 있는 것이다.
이러한 메커니즘의 가장 중요한 용도는 바로 VisualWorks가 빌드한 사용자 인터페이스에서 이루어진다. 모든 위젯 클래스는 (라디오 버튼, 텍스트 필드 등) 고정된 프로토콜을 말한다. 그들은 무엇을 표시할 것인지 알고 싶을 때 자신들의 모델로 value 메시지를 전송하고, 표시되는 내용을 변경하고 싶을 땐 value: 메시지를 모델로 전송한다.
대부분의 모델 객체들은 이러한 value/value: 프로토콜을 이해하지 못하므로 그들이 이해하는 무언가로 변환되어야 한다. 우리가 살펴볼 세 가지 클래스 각각은 이를 다른 방식으로 실행한다. 각 클래스는 한쪽에선 동일한 value/value: 프로토콜을 이해하지만 다른 쪽에선 다른 일을 한다. 위의 다이어그램이 이러한 점을 보여준다. 이에 해당하는 클래스를 하나씩 살펴보자.
ValueHolder 클래스
ValueHolder 클래스의 인스턴스들은 VisualWorks 위젯이 가장 단순한 모델과 작업하도록 허용한다. 기본적으로 그들은 String, Number 또는 Boolean의 인스턴스와 같은 단순한 모델 객체를 감싸는 '래퍼(wrapper)'로서, 그들이 일반적으로 이해하는 복잡한 프로토콜 대신 value 와 value: 로 응답하도록 만든다. ValueHolder의 인스턴스들은 어떤 객체로든 asValue 메시지를 전송함으로써 생성된다. 이를 확인하기 위해서는 Object 내 asValue 의 구현을 살펴보라.
ValueHolder로 value 메시지가 전송되면 해당 클래스는 그것이 감싸는 (그것의 '값') 객체를 리턴한다. 매개변수가 있는 value: 메시지를 수신하면 ValueHolder 클래스는 본래 값을 버리고 매개변수로서 전송된 객체로 대체한다. 그 의미를 주목하라. 위젯이 그 모델의 값을 '수정하길' 원할 경우(예: String), 모델은 변경되는 대신 교체된다. 이는 해당 모델을 사용하는 사람에게 중요한 의의를 가지며, ValueHolder를 이용해야 하는 또 다른 이유를 제공한다.
표시되는 실제 문자열이 그것에 관심이 있는 다른 모든 객체들에 의해 참조될 경우, 그것을 버리고 교체될 때마다 이 모든 객체들의 참조도 변경되어야 한다. 하지만 절대로 대체되지 않는 ValueHolder를 이용할 경우 모델을 사용하는 모든 객체들은 값이 변경된다 하더라도 같은 ValueHolder를 유지할 수 있다. 위의 그림은 ValueHolder가 어떻게 문자열을 ('Zebra') TextEditorView로 접속시켜 도메인 모델에서 접근하도록 해주는지를 보여준다.
이런 방식을 통해 ValueHolder의 인스턴스들은 아무리 내용(값)이 왔다 갔다 하더라도 떠나지 않는 컨테이너 역할을 한다. C 프로그래밍에 익숙하다면, 이를 포인터를 향한 포인터 (아무런 의미가 없으니 신경 쓰지 않아도 좋다!) 정도로 생각하면 되겠다.
ValueHolder의 인스턴스들은 객체에 대한 컨테이너 역할을 하는 동시 그 값이 변경되면 관계 당사자들에게 알려주는 역할도 할 수 있다. 이것이 가능한 이유는 제 8장에서 논한 의존성 메커니즘 덕분이다. ValueHolder는 심지어 onChangeSend:to: 메서드를 이용해 이것으로 편하게 연결하는 방법을 제공하기도 한다. 뿐만 아니라 그 값이 교체될 때마다 (ValueHolder 내에서 변경의 의미) 특정 객체에게 특정 메시지를 전송할 것을 ValueHolder에게 요청할 수 있도록 해준다. 예를 들어:
MyValHold onChangeSend: ttrefresh to: MyDomainModel.
이 표현식은 MyValHold가 value: 메시지를 수신하여 값이 변경될 때마다 MyDomainModel에게 refresh 메시지가 전송되도록 준비한다. ValueHolder가 다른 클래스, DependencyAdaptor를 이용해 이를 실행시키는 방식을 보고 싶다면 코드를 살펴볼 수 있겠다.
AspectAdaptor 클래스
AspectAdaptor는 ValueHolder와 비슷하지만, 좀 더 복잡한 모델에게 인터페이스를 제공한다. 가령 당신은 문자열, 숫자 등을 보관하는 여러 인스턴스 변수와 함께 클래스를 제공했을 수 있다. 그러한 인스턴스 변수에 대해 'get'과 'set' 메서드를 생성했을지도 모른다. 이러한 메서드명은 인스턴스 변수명과 동일해야만 insideLeg와 insideLeg: 라는 메서드를 가져 insideLeg 변수의 값을 얻고 설정할 수 있다 (아래 다이어그램 참조). 이제 TextEditorView에서 이러한 변수의 값을 표시하고 편집하길 원할 것이다. 하지만 뷰는 그것이 표시하는 객체 값을 얻고 설정하고자 할 때 value와 value: 메시지를 전송한다. 따라서 메시지를 당신의 클래스가 이해하는 것으로 변환할 방법이 필요하다. 그것이 바로 AspectAdaptor가 하는 일이다.
이러한 맥락에서 insideLeg 변수는 당신의 모델의 '측면'이라 부른다. AspectAdaptor의 업무는 모델의 한 가지 측면으로만 일반 용도의 뷰 객체를 접속하는 것이다. 이를 위해선 전송할 메시지를 알아야 할 필요가 있다. AspectAdaptor는 이러한 메시지들을 getSelector와 putSelector라 부른다 (selector, 선택자라는 용어는 메시지명을 참조 시 자주 사용된다). AspectAdaptor를 만들 때에는 forAspect: 메시지를 이용해 동시에 이 두 개의 메시지를 설정하거나 (이런 경우 getSelector는 당신이 부여한 기호로 설정되고, putSelector는 동일한 기호에 콜론(:)이 붙어서 설정된다), (좀 더 흔한 방법은) accessWith:assign:With: 를 구분하여 설정하는 방법이 있다. 당신은 subject: 메시지를 이용해 어떤 객체를 조정하고 있는지 AspectAdaptor에게 알려야 한다.
AspectAdaptor는 또한 '업데이트' 메시지도 전파할 것이다. 이를 위해선 subjectSendsUpdates:true 메시지를 어댑터로 확실히 전송되도록 하라. 이후 도메인 모델이 insideLeg의 값을 변경할 경우, 그것이 스스로에게 changed: #insideLeg 메시지를 전송하면 AspectAdaptor는 결과가 되는 update: 메시지를 뷰로 전달할 것이다.
PluggableAdaptor 클래스
PluggableAdaptor의 인스턴스들은 AspectAdaptor보다 한 단계 더 나아가 pluggability 의 개념을 취한다. 모델을 뷰로 적응시키기 위해 선택자를 제공 가능하도록 만드는 데 그치지 않고 스몰토크 코드의 전체 블록을 제공할 수 있다. 이러한 블록을 getBlock, putBlock, updateBlock이라 부른다. 이들은 PluggableAdaptor가 각각 value, value:, update:with:from: 메시지를 수신할 때 실행된다. 이는 모델을 뷰로 조정하는 데 엄청난 유연성을 제공한다. insideLeg 변수 내 인치로 값을 보관하는 모델로 뷰를 적응시키되 표시와 편집은 센티미터로 하길 원하는 경우를 예로 들어보자.
MyPA := PluggableAdaptor on: MyModel.
MyPA getBlock: [nn | m insideLeg * 2.5]
putBlock: [:m :v | m insideLeg: (value / 2.5)1
updateBlock: [:m :a :p | (a = ttinsideLeg) & (p > 34)].
가장 먼저 우리는 PluggableAdaptor의 인스턴스를 만들고 그것을 모델로(MyModel)로 연결한다. 이것은 위 블록에서 어떤 객체를 :m 매개변수로서 전송할 것인지 PluggableAdaptor에게 알려주는 동시 해당 객체의 종속자가 되도록 지시한다. 그러면 우리는 세 개의 블록 값을 설정한다.
getBlock이 단일 매개변수 :m-(MyModel이 될)모델-를 취한다. 이는 실행되는 즉시 insideLeg 메시지를 모델로 전송하고, 결과에 2.5를 곱하여 리턴한다. 그 결과, 뷰에 의해 PluggableAdaptor로 전송된 value 메시지는 모델에서 실행된 좀 더 복합 연산(complex operation)으로 적응되는 효과가 발생한다.
putBlock은 두 개의 매개변수, :ni와 :v-값(value: 메시지에 매개변수로서 전송된 객체)-을 취한다. 이러한 경우, insideLeg: 메시지를 이용해 값은 모델로 전송되기 전에 2.5로 나뉜다. 그 결과, PluggableAdaptor로 전송된 value: 메시지가 모델에서 실행된 또 다른 복합 연산으로 적응되는 효과가 발생한다.
마지막으로 모델로부터 업데이트 메시지를 수신할 때마다 PluggableAdaptor는 updateBlock를 실행한다. 이는 MyModel이 insideLeg 변수의 값을 변경하여 스스로에게 changed: #insideLeg with: insideLeg 메시지를 전송할 때에 발생할 수 있다. 이것은 모든 종속자들에게 insideLeg가 그 값을 변경하였음을 알리고 새 값을 전송한다.
PluggableAdaptor는 이러한 업데이트를 자신의 종속자들에게 (일반적으로 뷰) 전달할 것인지 결정해야 한다. 이러한 결정을 내리기 위해 updateBlock을 실행한다. 이것이 true로 평가되면 업데이트를 전달하고, false로 평가되면 업데이트를 전달하지 않는다. updateBlock이 그 외의 것으로 평가할 경우 오류가 발생하니 주의하길 바란다!
이러한 과정은 PluggableAdaptors가 그들의 모델로부터 수신할 수 있는 많은 업데이트를 필터링하여 뷰가 관심을 가진 내용들만 전달하도록 해준다. 이러한 과정이 필요한 이유는, 위의 다이어그램에서 보다시피 주어진 모델은 그것에 연결된 PluggableAdaptor의 인스턴스를 많이 가질 수 있기 때문이다. 모델은 그것의 어떠한 변수든 변경될 때마다 업데이트를 생성할 수 있으며, 모든 어댑터는 이러한 메시지를 수신할 것이다. 업데이트 메시지가 필터링되지 않았을 경우, 모든 뷰는 필요로 할 때에만 스스로를 새로고침(refresh)한다. 이는 화면에 깜빡거림을 야기할 수도 있다.
PluggableAdaptor의 인스턴스들은 사실상 이중적인 생활을 한다. 우선 모델을 뷰로 연결하면서 뷰에겐 모델처럼 보이고 모델에겐 뷰처럼 보인다. 그들의 블록에 위치할 수 있는 코드는 기본적으로 제한이 없으므로 매우 강력한 기능이 된다. 하지만 블록에 위치한 코드는 하나의 인스턴스로만 적용하여 항시 모델 객체 내 코드에 비해 디버깅이 힘들기 때문에 주의해야 한다 (제 15장-스몰토크 코드 디버깅하기를 참조).
재사용은 OOP의 주요 혜택 중 하나에 해당하고, 스몰토크에선 재사용을 확보하는 방식들 중 하나로 pluggability 를 들 수 있다. 이번 장에서는 클래스 라이브러리 내에서 이식 가능한 세 가지 클래스만 집중적으로 살펴보았다. 이들 클래스 각각은 value, value:, 'update' 메시지를 좀 더 적절한 것으로 적응시킨다. 다시 말해, 일반적으로 value와 value: 를 전송하는 뷰처럼 보이는 객체들을 (버튼, 텍스트 에디터 등) 이러한 메시지를 직접 이해하지 못하는 모델 객체로 연결할 수 있다는 말이다.
이러한 어댑터 클래스가 어떤 일을 하는지 세부적으로 이해하지 못한다 하더라도 염려하지 말길 바란다. 많은 경우 그들의 기능은 숨겨져 있으며 당신이 걱정하지 않아도 된다. 일반 원칙만 이해하면 이러한 일을 수행해야 할 때, 그리고 수행하는 동안 정확히 무슨 일이 벌어지는지 이해할 수 있을 것이다.
클래스 라이브러리 내 다른 많은 클래스도 이식 가능하며, 이들이 어떻게 작용하는지 언제든 살펴보면 된다. 단, 그러한 pluggability 는 클래스 라이브러리에서 주목한지 얼마 되지 않은 기능들 중 하나기 때문에 그것이 구현되는 방식과 클래스를 명명하는 방식에 있어 약간의 일관성이 떨어진다.
마지막으로, 이번 장에서 제시한 개념을 정말로 이해했다면, 다른 프로그래머들에 의해 일반적인 방식으로 재사용되는 클래스를 빌드하는 경우 pluggability 를 고려해야 한다는 사실을 깨달았을 것이다.