DesignPatternSmalltalkCompanion:Decorator
DECORATOR (DP 175)
의도
객체에 동적으로 책임과 행위를 추가할 수 있게 한다. Decorator 패턴은 기능의 확장을 위해 상속 대신 사용할 수 있는 유연한 대안 방법을 제공한다.
구조
논의
Decorator로서 구현되는 클래스는 스스로 핵심 기능성을 제공하지는 않는다. 대신 Decorator 인스턴스가 감싸는 객체의 핵심 기능성을 향상시킨다. Decorator와 그것이 감싸는 객체는 (그것의 Component) 동일한 슈퍼타입을 갖는다ㅡ다시 말해 동일한 핵심 인터페이스를 갖는다. 따라서 클라이언트는 일반적으로 Component와 그것의 Decorator를 구별하지 못한다.
Decorator 클래스를 알아보는 방법은 Decorator의 타입과 동일한 타입을 가진 인스턴스 변수를 가진 Decorator 클래스를 찾으면 된다; 즉, Decorator와 그 컴포넌트는 동일한 Component 인터페이스를 가지는 것이다. Decorator는 그의 구현을 인스턴스 변수로 위임하지만, 그 과정에서 단순한 위임에 (또는 대신에) 추가로 행위를 더하기 위해 메시지 일부를 구현하기도 한다.
데코레이터라는 이름에서는 텍스트 뷰 주위에 스크롤바를 감싸는 것처럼 비주얼과 꼭 함께 사용해야 할 것을 암시하지만, 사실상 다목적으로 사용할 수 있다. 이 패턴을 이용 시 비주얼뿐만 아니라 어떤 유형의 객체에도 기능성을 추가할 수 있다.
시스템 패턴
Decorator 패턴은 흔히 사용되는 패턴이지만 꽤 시스템 지향적인 패턴이라 할 수 있는데 이는 윈도윙 시스템, 스트림, 폰트와 같이 보통 기본적인 시스템 프레임워크의 구현에 사용됨을 의미한다. 하지만 모델링 영역에서 사용되는 경우는 드물기 때문에 스몰토크 애플리케이션 개발자들은 자주 사용하지 않는다. 비주얼웍스는 다른 방언보다 훨씬 더 세게 기반 프레임워크를 구현할 때 Decorator 패턴을 사용하므로, 이 패턴과 관련된 대부분 스몰토크의 예들은 비주얼웍스에서 사용예가 되겠다. 보험금 한도액에 관한 예제를 살펴보기 전에 Decorator가 일반적으로 어떻게 작용하는지 살펴보고자 한다.
Wrapper 계층구조
비주얼웍스의 Wrapper 계층구조는 전형적인 Decorator 패턴의 예이다. 비주얼웍스는 [디자인 패턴] (DP4-6)와 팩토리 메소드에서 (DP 107) 논했던 모델-뷰-컨트롤로(MVC) 프레임워크를 이용해 그래픽 사용자 인터페이스를 구현한다. MVC에서 모델은 표시할 수 있는 상태를 포함하고, 뷰는 상태를 표시하며, 컨트롤러는 상태를 조작하는 입력을 처리한다. MVC 프레임워크는 여러 디자인 패턴을 구현하는데, 여기에는 Decorator 뿐만 아니라 Observer (305), Composite (137), Strategy (339)도 포함된다.
MVC에서 뷰는 VisualComponent 계층구조로서 구현된다; 그것의 메인 클래스를 위에 표시하였다. VisualComponent의 메인 하위계층구조는 VisualPart이다. VisualPart에는 DependentPart나 SimpleComponent와 같은 잎 서브클래스가 있고, CompositePart와 같은 복합 서브클래스(Composite 패턴의 (137) 예제), Wrapper와 같은 Decorator 서브클래스가 있다.
Wrapper는 그 컴포넌트의 행위 중 어떤 것도 변경하지 않고 Decorator 패턴을 구현한다; 실제로 컴포넌트들의 행위를 변경시키는 Wrapper의 서브클래스이다. Wrapper는 component라고 불리는 인스턴스 변수를 가지는데, 이것은 VisualComponent에 해당한다. 패턴을 구현 시 당신은 무언가 특별한 일이 발생하는 것을 알아차릴텐데 그 이유는 Wrapper는 VisualComponent의 서브클래스이지만 그 인스턴스 변수 또한 VisualComponent이기 때문이다. 이는 Visualcomponent들을 컴포넌트로 가지는 VisualComponent들의 (Wrapper들) 재귀적 구조를 형성한다. Wrapper는 VisualPart와 VisualComponent로부터 다양한 키 메시지를 하위구현(subimplement)하지만, 그러한 메시지들의 구현은 그 컴포넌트로 동일한 메시지를 전달하는 일에 지나지 않는다. [1]Wrapper가 하는 일은 이것이 전부다. 따라서 Wrapper에 감싸진 비주얼은 래퍼가 없을 때와 똑같이 행동한다.
Decorator 계층구조의 상위 클래스에서는 이러한 실제 행위의 결여가 자주 발생한다. 클래스는 고작해야 인스턴스 변수를 정의하고 키 메시지를 인스턴스 변수로 전달하기 위해 메시지를 오버라이드한다. Decorator 서브클래스들은 일반적인 서브클래스로 구현된다; 이들은 행위를 슈퍼클래스의 행위를 변경하기 위해 슈퍼클래스를 하위구현한다. Decorator 서브클래스의 경우, top 클래스의 일반 데코레이션 행위는 아무 것도 하지 않고 구체적 데코레이션 행위를 추가한다. 이것은 Wrapper 뿐만 아니라 모든 Decorator 계층구조에서 관찰할 수 있다.
Wrapper가 Decorator 패턴을 정의하고 나면 그 서브클래스는 뷰와 컨트롤러의 행위에 모두 영향을 미치는 엄청난 자유성을 가진다. 모든 서브클래스가 해야 하는 일은 Wrapper 서브클래스가 그 컴포넌트로 전달하는 모든 메시지의 구현을 변경하는 것이다. 예를 들어, BoundedWrapper는 그 비주얼의 경계ㅡ그에 할당된 화면 공간ㅡ를 설정한다. BoundedWrapper로 비주얼을 둘러쌈으로써 래퍼는 비주얼의 경계를 재정의한다. 클라이언트가 비주얼의 경계가 무엇인지 컴포넌트에게 물으면 래퍼는 메시지를 중간에서 가로챈 후 아래 그림과 같이 비주얼의 경계 대신 자신의 경계를 리턴한다:
Wrapper 서브클래스에 흔히 사용되는 다른 방법들이 있다. ReversingWrapper는 그것을 그릴 때 주요 위치와 배경 색상을 전환함으로써 비주얼을 강조한다. GraphicsAttributeWrapper는 색상이나 라인 폭과 같은 그래픽 속성을 설정한다. PassivityWrapper는 활성화/비활성화 모드로 켰다 껐다 할 수 있다. 활성화된 경우, 이는 메시지에 영향을 미치지 않고 전달한다. 비활성화된 경우, 회색으로 그림으로써 비주얼을 비활성화된 것처럼 보이게 만들고, 모든 컨트롤러 메시지를 차단시켜 비활성화된 것처럼 작동하도록 만든다. 따라서 Wrapper를 Decorator 클래스로 단순히 소개함으로써 Wrapper를 클래싱하고 비주얼의 모든 속성을 통제하는 것이 가능하다.
중첩된 Decorator
단일 Component는 그에 중첩된 다수의 Decorator를 가지는 것이 보통이다. 예를 들어, 비주얼웍스에서 비주얼에 스크롤링을 추가하고자 하는 경우 비주얼웍스는 그 주위를 ScrollWrapper로 감싼다. 하지만 ScrollWrapper 주위에 BorderedWrapper를 감싸기도 한다. BorderedWrapper는 비주얼의 경계를 설정하고 그 경계의 윤곽을 그리는 가장자리를 그리는 BoundedWrapper의 서브클래스이다. ScrollWrapper 주위에 BorderedWrapper를 감싸면 스크롤이 가능한 영역의 경계선을 설정하고 그 경계선을 시각적으로 표시한다.
Decorator의 순서는 중요하다. ScrollWrapper가 BorderedWrapper를 감싸고 있는 경우 경계가 있는 직사각형을 스크롤할 수 있다; 하지만 그다지 잘 작동하진 않을 것이다.
추상적 잎 클래스와 복합 클래스
Abstract Leaf and Composite Classes
패턴의 예제들은 많은 공통된 구현을 공유하는 수많은 ConcreteComponent 구체적 클래스들을 가지곤 한다. 예를 들어, 오퍼레이션이 주로 가지는 기본 최소단위의 구현은 모든 ConcreteComponent에 적절하나 Decorator에겐 적절하지 않은데 그 이유는 그들의 컴포넌트에 오퍼레이션을 위임해야 하기 때문이다.
여기서 문제는 공통된 행위를 어디서 구현하는지가 된다. Component에 행위를 구현하면 모든 ConcreteComponent 클래스뿐만 아니라 Decorator 클래스에 의해서도 상속될 것을 의미한다. 하지만 ConcreteComponent 클래스로 행위의 구현을 미루면 동일한 코드를 여러 번 복제해야 하는 필요성이 생긴다.
이보다는 모든 ConcreteComponent 클래스에 공통된 “Decoratee” 슈퍼클래스가 더 나은 해결책이 된다. 주로 추상적 클래스인 이 슈퍼클래스는 모든 ConcreteComponent 클래스에 공통되지만 Decorator 클래스에는 적절하지 않은 코드는 구현한다. 구조 다이어그램에서는 이미 모든 ConcreteDecorator들에게 공통되는 코드를 구현하기 위한 Decorator 클래스를 포함하고 있다. 다음 페이지에 실린 그림과 같이 Decoratee 추상적 클래스를 추가하면 다이어그램은 다음과 같은 모습을 띨 것이다:
Decorator는 Component의 서브클래스이다
Decorator 패턴에는 한 가지 혼란스러운 면이 있는데, 이는 Decorator 클래스는 그것이 감싸는 Component 추상 클래스의 서브클래스라는 점이다. Decorator 인스턴스는 그것이 감싸는 컴포넌트의 부모이기 때문에 반직관적이다. Component 위에 Decorator가 있는 일반적인 트리 구조를 나타내는 다음 다이어그램을 고려해보자. 각 Decorator는 그의 Component들의 부모이다; 그러므로 Component는 Decorator의 자식이 된다. 자연스레 Component 클래스는 Decorator 클래스의 서브클래스가 된다. 하지만 Decorator 클래스가 충족해야 하는 인터페이스는 Component 클래스가 정의하므로 Component 클래스는 Decorator 클래스의 슈퍼클래스가 된다:
Decorator 패턴의 설명을 시작할 때 표시한 클래스 다이어그램과 위의 객체 다이어그램 간 차이를 명심하라. 클래스 계층구조는 계층구조 브라우저에서 당신이 보게 되는 것이다; 객체 다이어그램은 탐색기(inspector) 또는 탐색기 시리즈에서 보게 되는 것을 의미한다. 클래스 다이어그램은 정적이다; 그리고 당신이 클래스 정의를 재컴파일하지 않는 한 계층구조는 계속 이런 형태를 띨 것이다. 객체 다이어그램은 동적이다; 따라서 인스턴스는 많은 조합으로 구성될 수 있으며 이는 전형적인 하나의 예에 불과하다. 클래스 구조에서는 Component가 Decorator의 슈퍼클래스이다. 반면 객체 구조에서는 Decorator가 Component의 부모가 된다.
단일 Decorator 클래스
C++와 같이 강한 타이핑 언어에서는 사실상 Decorator 패턴을 Component 계층구조의 구분된 하위계층구조로 구현할 것을 요구한다. 반면 스몰토크는 이러한 제약을 강요하지 않는다. 예를 들어, 비주얼웍스의 ValueModel 계층구조에는 다음과 같은 Decorator 클래스 3개를 포함한다: BufferedValueHolder, RangeAdaptor, TypeConverter. 대부분의 ValueModel 클래스는 Adapter (105) 패턴으로, Adaptor 또는 (주제대상; subject)는 주로 Object의 어떤 유형이든 될 수 있다. 하지만 위의 ValueModel 클래스 3가지는 그들의 주제대상들이 다른 ValueModel일 것으로 예상하므로 Adapter가 아니라 Decorator로 볼 수 있다. 아래 다이어그램에서 나타내는 바와 같이 ValueModel Decorator 클래스들은 계층구조에서 서로 전혀 연관되어 있지 않다 하더라도 Decorator로서 작용한다:
스몰토크에서 굳이 요구하는 것은 아니지만 동일한 계층구조 내에 다수의 Decorator 클래스들은 공통된 Decorator 슈퍼클래스를 가져야 한다. 그렇지 않을 경우 각 ConcreteDecorator 클래스가 자신의 Decorator 행위를 정의할 수밖에 없다. Wrapper 계층구조의 어떠한 서브클래스도 Decorator 행위를 정의해선 안 되는데, 그 이유는 공통된 슈퍼클래스인 Wrapper로부터 행위가 상속되기 때문이다. 서브클래스가 할 일은 비주얼을 어떻게 감쌀 것인지 정의하는 일뿐이다. 이와 반대로, 3개의 ValueModel Decorator 클래스들은 공통된 ValueModelDecorator 슈퍼클래스의 서브클래스였을 시 구현했을 법한 행위를 한 번만 중복시킨다.
공통된 Decorator 클래스가 없을 시 또 다른 단점으로는 클래스가 Decorator 패턴을 따르는지 인식하기가 어렵다는 점이다. 예를 들어, BufferedValueHolder는 버퍼가 있는 ValueHolder처럼 보인다. 이는 사실상 Decorator 패턴에 반하는 예제(anti example)로 보인다ㅡValueHolder를 확장하는 데에 Decorator 대신 서브클래싱을 사용하기 때문이다. BufferedValueHolder는 ValueHolder의 서브클래스로 구현되긴 하지만 그 안에 ValueHolder가 구축되어 있지 않다. 대신 그 주제대상이 ValueModel이 될 것이라 기대한다ㅡValueHolder을 포함해 어떠한 유형의 ValueModel이든 해당한다. BufferedValueHolder는 모든 종류의 ValueModel에 버퍼를 추가할 때 사용된다. 물론 (“BufferedValueHolder” 대신) “BufferedValueModel”라고 불리고, ConcreteComponent의 서브클래스 대신 Decorator 가지에 위치한 서브클래스였다면 훨씬 더 명확했을 것이다.
아래 다이어그램은 Decorator 패턴을 더 잘 구현하는 가상의 ValueModel 계층구조이다. Decorator 패턴을 구현하기 위해 ValueModelDecorator 클래스를 도입하였고, 그의 서브클래스로 BufferedValueHolder, RangeAdaptor, TypeConverter를 만들었다. ValueModel이 Component와 Concretecomponent 클래스를 결합하는 것을 주목하라.
ValueModel Decorator들은 Decorator의 중첩(nesting)이 가진 힘을 보여주는 또 다른 예이다. ValueModel에서 버퍼가 필요한 경우 당신은 BufferedValueModel로 그것을 감싼다. 그리고 그것의 타입을 변환할 필요가 있을 경우 당신은 TypeConverter로 감싼다. 값을 보호하거나 그 타입을 변경할 때에는 각각 BufferedValueHolder과 TypeConverter를 사용하면 훨씬 더 좋다. 이번 사례에서 순서는 결정적이진 않지만 여전히 중요하다. BufferedValueHolder가 TypeConverter를 감싸면 변환된 값이 버퍼되지만, 그 반대의 경우 변환되지 않은 값을 버퍼한다.
Decorator의 핵심 인터페이스
Component 클래스는 Decorator와 ConcreteComponent 서브클래스가 구현하는 핵심 인터페이스를 정의해야만 한다. 어떤 핵심 인터페이스를 어떻게 구현하는지에 관한 자세한 정보는 Composite (137) 패턴을 참조한다.
Composite 패턴에서와 마찬가지로 Decorator 구조 내 서브클래스는 자신의 Component의 인터페이스 확장을 피해야 한다. Decorator 서브클래스가 Component의 인터페이스를 확장할 경우 클라이언트는 Decorator의 확장된 인터페이스를 사용하기 전에 컴포넌트가 그 위에 이러한 타입의 Decorator를 갖고 있는지 확인해야 한다.
보험금 한도액
모델링 도메인에 Decorator를 사용하는 경우는 드물지만 예를 하나 들어보고자 한다. Decorator는 보험료 청구 시 보험 계약자에게 지급할 한도액을 정하기 위해 보험 분야에서 사용되어 왔다. 보험 회사 정책은 다양한 의료절차에 대해 여러 요율로 배상금을 지급한다. 정책은 한 번의 클레임에 대해 한도액(cap)이라 불리는 최대금액을 정한다. 이 정책에 계약한 이들은 동일한 한도액을 가지며, 정책으로 구축될 수 있다. 하지만 동일한 정책의 다양한 보험 계약자들이 다른 한도액을 가지거나 한도액을 정책으로 구축하기가 매우 까다로운 경우, 한도액을 정책에 Decorator로 구현할 수 있다.
Policy는 클레임을 처리하여 배상금액을 계산하는 reimbursementForClaim: 과 같은 메소드를 가질 것이다. 보험 계약자마다 다른 한도액을 가지기 때문에 이 메소드는 보험 계약자를 바탕으로 하여 모든 한도액을 무시할 것이다. 따라서 일부 보험 계약자의 경우 배상금액이 한도액을 초과하기도 한다. 이를 미연에 방지하기 위해 각 Policyholder는 Policy가 아니라 Policy를 쥐고 있는 PolicyCap을 유지한다. PolicyCap가 reimbursementForClaim:을 구현하면 Policy로부터 배상금액을 얻고, 한도액과 비교하여 검사한 후 둘 중 적은 값을 리턴한다.
이 예제에 적용되는 코드를 소개하겠다. 가장 먼저 우리는 Component 클래스, 즉 ConcreteComponent와 Decorator에 대한 인터페이스를 정의하는 추상 슈퍼클래스를 선언한다. 이를 Component AbstractPolicy라고 부르고, 더 흥미로운 reimbursementForClaim: 메시지를 구현해보겠다.
Object subclass: #AbstractPolicy
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
AbstractPolicy>>reimbursementForClaim: aClaim
"Calculate and return how much money
the policy will pay for aClaim."
^self subclassResponsibility
다음으로, Policy라는 ConcreteComponent 클래스를 구현해본다. 이 클래스는 정책과 같은 방식으로 작용하므로 구현하기 수월하다. reimbursementForClaim:의 자세한 내용은 오히려 복잡해질 뿐더러 Decorator 패턴과 그다지 상관없기 때문에 생략하겠다.
AbstractPolicy subclass: #Policy
instanceVariableNames: '...' "reimbursement variables"
classVariableNames: ''
poolDictionaries: ''
Policy>>reimbursementForClaim: aClaim
"See superimplementor."
"... code to calculate the reimbursement ..."
마지막으로 Decorator 클래스인 PolicyCap을 구현할 것이다. Decorator 이기 때문에 그의 Component를 가리키는 인스턴스 변수가 필요하다. 이를 policy라 부르겠다. 배상 한도액을 유지하기 위한 인스턴스 변수도 필요하다.
AbstractPolicy subclass: #PolicyCap
instanceVariableNames: 'policy capAmount'
classVariableNames: ''
poolDictionaries:
PolicyCap은 두 단계에 걸쳐 배상금을 계산한다. 첫째, policyCap은 얼마의 금액을 배상해야 하는지 poilcy에게 질문한다. 둘째, 그 금액과 한도액 중 적은 금액을 리턴한다.
PolicyCap>>reimbursementForClaim: aClaim
"See superimplementor."
| uncappedAmount cappedAmount |
uncappedAmount := self policy reimbursementForClaim: aClaim.
cappedAmount := uncappedAmount min: self capAmount.
^cappedAmount
다른 인스턴스 함수 또한 Decorator로 구현이 가능하다. 예를 들자면, PolicyDeductible은 배상금액에서 보험 계약자의 공제 조항에 따른 금액과 고용인 부담금액을 제한다.
Stream Decorators
[디자인 패턴] 편에 실린 Decorator 패턴의 알려진 사용예 단락에서는 StreamDecorator 클래스(DP183)를 논하고 있다. StreamDecorator는 ASCII stream를 압축하고 8비트 ASCII를 7비트 ASCII로 변환할 수 있는 서브클래스를 가진다.
스몰토크 개발자들은 Stream 데코레이터에 사용 가능한 예제를 발견하였다. Stream은 주로 문자와 바이트를 저장하는데 사용되는데 이는 플랫 파일이 이들을 보관하기 때문이다. 하지만 스몰토크에서는 여전히 문자나 바이트가 아닌 복잡한 객체를 보관한다. 이러한 객체를 파일로 어떻게 저장할까?
비주얼웍스는 BOSS라고 불리는 프레임워크를 제공하여 (이진 객체 스트리밍 서비스) 파일로 객체를 저장한다. BOSSTransporter 계층구조는 객체를 읽고 쓸 수 있는 스트림과 같은 클래스들의 집합이다. BOSSReader라는 서브클래스는 next 프로토콜을 구현하고, BOSSWriter라는 서브클래스는 nextPut: 프로토콜을 구현한다. 이 두 개의 서브클래스는 그들의 행위를 구현하기 위해 stream이라 (ReadStream 또는 WriteStream 중 하나) 불리는 인스턴스 변수를 이용한다. 예를 들어, BOSSWriter는 각 객체를 BOSSBytes로ㅡ특수 ByteArrayㅡ전환한 후에 WriteStream를 사용하여 바이트를 파일로 쓴다.
BOSSTransporter는 Component 클래스들과 동일한 계층구조에서 구현되지 않는 Decorator 클래스의 또 다른 예를 제공한다. Stream과 BOSSTransporter는 Object의 서브클래스들이다; 이것이 그 둘의 유일한 공통점이다. 위의 예제를 C++에서 적용할 경우 강한 타이핑 언어의 특성상 잘 작동하지 않을 것이다; 클라이언트는 그것이 Stream을 쓰는지 아니면 BOSSTransporter를 쓰는지 알아야 할 것이다. 이 둘은 하나의 Component 계층구조에 있으므로 서로 교체해서 사용할 수도 없다. Java의 경우, “스트리밍” 인터페이스를 선언하고 이를 구현하기 위해 Stream과 BOSSTransporter를 모두 선언했다면 처리가 가능하다.
스몰토크가 Decorator들과 Component들을 완전히 구분된 계층구조에서 처리할 수 있다고 해서 꼭 훌륭한 방법은 아니다. 둘을 동일한 계층구조에 위치시키는 것은 동일한 인터페이스를 지원할 것을 의미하며, 이는 클라이언트가 둘을 교체해서 사용할 수 있음을 의미한다. Component와 Decorator 계층구조가 구분되어 있을 경우, 두 계층구조는 그들의 인터페이스들을 다형적으로 유지하도록 관리되어야 한다. 가능한 한 이중 관리(dual maintenance)는 피하는 편이 좋다.
파일 리더 프레임워크은 (Woolf, 1997) Stream Decorator의 훌륭한 예를 제공한다. 이는 FormattedStream을 Stream의 서브클래스로서 구현한다. FormattedStream은 Stream로서 next, nextPut:, atEnd와 같은 일반 메시지들을 구현한다. 주요 인스턴스 변수는 Stream의 인스턴스인 dataStream이다. FormattedStream은 nextPut:을 구현하여 객체를 수용하고, StreamFormatDe를 이용해 객체를 레코드로 변환한다. 그리고 나서 변환된 레코드를 파일로 쓰기 위해 dataStream을 이용한다. FormattedStream>>next는 이 과정을 거꾸로 실행하여 파일로부터 레코드를 읽어낸 후 이를 하나의 객체로 변환시킨다. dataStream은 어떠한 타입의 Stream이든 될 수 있으므로 “파일”은 가로질러 스트림이 가능한 객체는 무엇이든 될 수 있다.
놓친 기회들
Decorator 패턴을 사용한 예제에는 모두 놓친 기회가 있는 것으로 보인다. Decorator 패턴의 핵심은 상속을 대신할 수 있는 유연한 대안책이라는 점이다. 클래스의 서브클래스를 생성시킴으로써 행위를 추가하는 대신 우리는 Decorator를 생성시킴으로써 행위를 추가할 수 있다. 그리고 난 후 그 Decorator는 동일한 타입의 다른 클래스를 감싸는데 사용될 수 있다. 그뿐 아니라 Decorator들은 중첩이 가능하므로 서브클래스 또는 그 피어(peer) 중 하나를 택하기보다는 둘 다 감쌀 수 있다.
상속을 대신하는 유연한 대안책을 사용하였더라면 더 유익한 결과를 가져왔을 계층구조가 너무도 많다. 예를 들어 다음 페이지에 표시한 다이어그램과 같이 비주얼웍스의 Stream 계층구조에서 ExternalReadStream은 CodeReaderStream이라 불리는 서브클래스를 가지며, ExternalWriteStream은 CodeWriterStream이라 불리는 서브클래스를 갖는다.
CodeReaderSream과 CodeWriterStream 프로토콜은 서로 대응하지만 이러한 상호간 행위는 주로 단일 클래스에 캡슐화된다. 하지만 CodeStream이라는 단일 클래스가 있었다면 어떻게 ExternalReadStream들과 ExternalWriteStream들과 함께 사용되었을까? 이것은 ET++(DP 183)에서와 상당 부분 비슷하게 Stream Decorator로서 구현되어야 한다.
그 점에 대해서는 Stream 계층구조에서 읽기-쓰기 행위와 내부-외부 구현 간 구분 및 결합은 서브클래싱과 관련된 최악의 사례가 될 것이다. 가장 먼저, 계층구조는 내부와 외부 계층구조로 나뉜다; 그리고 난 후 하위계층구조들은 읽기, 쓰기, 첨부하기, 조합하기와 같은 행위에 대한 구현을 중복한다. 이러한 행위들이 Decorator로 구현되었다면 원하는 조합 어디든 내부와 외부 스트림 주위를 감싸야 한다.
다른 예를 하나 더 소개하겠다. 비주얼웍스의 ApplicationModel은 유용한 행위를 추가하는 추상 서브클래스를 몇 개를 갖고 있다: SimpleDialog는 모델 윈도우를 생성하고; LensApplicationModel는 Object Lens 프레임워크와 접속하며; 제3자 벤더(vendor)는 ValueInterface와 같은 고유의 추상 서브클래스를 추가해왔다 (Abell, 1996). 하지만 당신이 만약 Lens를 이용할 수 있는 모달(modal) 윈도우를 원할 경우 어떻게 될까? SimpleDialog와 LensApplicationModel 중 하나를 서브클래스로 둘 것인가?
이러한 클래스들이 ApplicationModel Decorator로서 구현되었다면 당신은 둘을 쉽게 이용할 수 있다. 일반 윈도우라면 Applicationmodel의 서브클래스를 선언했을 것이다. 윈도우의 인스턴스가 모달이어야 할 경우 이를 SimpleDialog Decorator로 감싼다. 또 다른 인스턴스가 Lens를 이용해 그 데이터를 수집할 필요가 있는 경우라면 이를 LensApplicationModel Decorator로 감싼다. 둘 다 필요한 경우 두 Decorator를 중첩시킨다.
Collection 계층구조는 비유연한 상속의 사용예로 Decorator 패턴을 사용 시 유용한 또 다른 예이다. 집합체를 저장하는 방법에는 기본적으로 4가지가 있다: 배열 (Array, OrderedCollection), 연결 목록 (비주얼웍스의 LinkedList와 Link), 해시표 (Set), 또는 (균형 이진) 트리로 저장할 수 있다. 다양한 Collection 클래스들은 이러한 기본 저장 구조를 확장시킨다; 중복 제거 (Set), 분류 (SortedCollection), 의존관계 알림 (비주얼웍스의 List) 구조가 있다. 단, 순서는 보존하되 중복을 제거하기 위해 집합체가 필요한 경우 문제가 발생한다. 당신은 아마도 OrderedSet를 OrderedCollection의 서브클래스로서 구현해야 할 것이다. 하지만 분류와 중복 제거를 위해 집합체가 필요하다면 SortedSet 또한 SortedCollection의 서브클래스로 구현해야 할 것이다. OrderedSet와 SortedSet의 코드는 매우 비슷할 것이다; 주요 차이점이라면 슈퍼클래스가 되겠다.
좀 더 유연한 해결책으로는 Collection 계층구조를 ConcreteComponent와 Decorator 가치로 나누는 방법이 있다. ConcreteComponent 클래스들은 배열, 연결 목록, 해시표, 어쩌면 트리도 구현할 수 있을 것이다. Decorator는 중복 제거, 분류, 의존성 알림을 구현할 것이다. 그리고 나면 “정렬된 집합”은 중복 제거 데코레이터를 사용한 배열일 것이다. “분류된 집합”은 분류 데코레이터에 중복 제거 데코레이터를 이용한 배열이겠다. (사실상 OrderedCollection은 “성장이 가능한 집합체” 데코레이터로 둘러싸인 Array가 될지도 모른다.) 이러한 해법은 최근 Collection 계층구조의 상속 접근법보다 효율성은 조금 덜할지 몰라도 확실히 더 유연한 방법이다. (집합체의 구현을 위한 IBM의 접근법은 Bridge (121)를 참조한다.)
구현
Decorator를 구현하고자 할 때는 [디자인 패턴] 편에 언급한 문제 이외에도 고려해야 할 문제가 몇 가지 더 있다:
- Decorator 슈퍼클래스를 사용하라. Decorator 하위계층구조는 그 기반에 추상적 Decorator 클래스를 필요로 하는 것은 아니지만 매우 유용하다. Component 계층구조에 있는 추상적 Decorator 클래스는 Decorator 클래스가 패턴을 더 쉽게 구현하고, 코드를 검토하거나 유지하는 사람이면 누구나 명확하게 패턴을 확인할 수 있도록 해준다. ValueModel 계층구조에 걸쳐 흩어져 있는 ValueModel Decorator 클래스들을 보라. Decorator처럼 보이는가? BOSSTransporter 계층구조는 Stream 계층구조로부터 완전히 분리되어 있다. 이것이 Stream Decorator처럼 보이는가? Component의 Decorator 서브클래스는 패턴을 더 쉽게 구현하도록 해줄 뿐 아니라 인지와 관리 또한 수월하게 해준다.
- ConcreteComponent 서브클래스를 고려하라. Component의 구분된 ConcreteComponent 서브클래스는 Component 계층구조를 두 개의 구별된 하위계층구조로 분리시킬 것이다: 감쌀 수 있는 클래스들은 Decorator로, 감싸는 대상이 될 수는 있지만 자체가 데코레이터는 아닌 클래스들은 ConcreteComponent로 분리한다.
- Decorator만이 그 컴포넌트로 위임한다. 모든 Decorator 서브클래스들은 Decorator의 위임 메소드를 상속시킴으로써 그 컴포넌트에게 기본 행위를 미뤄야 한다. 이는 확장될 수 있는 메시지를 특정 Decorator 서브클래스가 실제로 변경시키는 메시지로부터 분리시킬 것이다.
- Decorator 서브클래스가 ConcreteComponent로 직접 위임할 것이라고 가정하지 말라. Decorator는 중첩될 수 있으므로 Decorator는 다른 Decorator로 위임될 수도 있다.
- 3가지 전달 방식이 있다. Decorator는 그 컴포넌트로 오퍼레이션 메시지를 3가지 방식으로 전달할 수 있다:
- 단순 전달: 다른 행위를 실행하지 않고 컴포넌트로 메시지를 전달한다.
- 확장 전달: 메시지를 컴포넌트로 전달하기 전과 (전이나) 후에 추가 행위를 실행한다.
- 오버라이드: 컴포넌트로 메시지를 전달하는 대신 행위를 수행한다; 그 행위가 아무런 일을 하지 않을 수도 있다.
예제 코드
파일 리더의 메인 계층구조인 FieldFormatDescription은 Decorator 패턴의 교과서적 예제를 포함한다. FieldFormatDescription은 필드를 파일로부터 어떻게 읽어야 하는지를 알고 있다. 또한 필드가 구획이 정해져 있는지 아니면 길이가 고정되어 있는지, 구분 기호 또는 길이는 무엇인지 등을 알고 있다. 그 중에서도 이러한 설명은 필드를 어떻게 읽고 쓰는지 알고 있다는 점이 중요하다. 그것의 서브클래스 3개는 LeafFieldFormat, compositeFieldFormat (Composite (137)의 예제), FieldFormatDecorator가 된다. 아래 다이어그램이 계층구조를 나타낸다:
패턴이 어떻게 구현되는지 살펴보자. 슈퍼클래스 FieldFormatDescrition은 전체 계층구조에 대한 핵심 인터페이스를 구현한다. 이는 Component 클래스의 역할을 충족한다. 이것은 추상적 클래스로서 readObjectFrom:into: 와 adaptorForIndex:andSubjectChannel:와 같은 기본 메시지를 정의한다.
Object subclass: #FieldFormatDescription
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
FieldFormatDescription>>readObjectFrom: dataStream into: aValueModel
self subclassResponsibility
FieldFormatDescription>>adaptorForIndex: anInteger
andSubjectChannel: aValueModel
^(IndexedAdaptor subjectChannel: aValueModel)
forIndex: anInteger
FieldFormatDecorator는 FieldFormatDescription의 서브클래스로서 Decoarotor 클래스를 구현한다. 이것은 fieldFormat이라는 인스턴스 변수를 가지는데, 이 변수는 Decorator의 컴포넌트를 향한 포인터 역할을 한다. 이것은 FieldFormatDescription 핵심 메시지들을 자신의 fieldFormat으로 위임을 통해 구현한다.
FieldFormatDescription subclass: #FieldFormatDecorator
instanceVariableNames: 'fieldFormat"
classVariableNames: ''
poolDictionaries: ''
FieldFormatDecorator>>readObjectFrom: dataStream into: aValueModel
fieldFormat
readObjectFrom: dataStream
into: aValueModel
FieldFormatDecorator>>adaptorForIndex: anInteger
andSubjectChannel: aValueModel
^fieldFormat
adaptorForIndex: anInteger
andSubjectChannel: aValueModel
필드를 읽고 쓰는 과정에서 필드의 설명에 추가할 수 있는 유용한 행위가 많으며, 계층구조는 이러한 행위들을 Decorator로 구현한다. RecordFieldFormat은 마지막에 레코드 구획 분리를 예상하기 위해 설명을 향상시킨다 (주로 CompositeFieldFormat). readObjectFrom:into:를 오버라이딩함으로써 레코드를 구분된 스트림으로 읽은 후 그로부터 필드(들을)를 읽을 수 있도록 하는 것이다. 이 메소드는 fieldFormat으로 직접 위임하지 않는다는 사실을 명심하라; 슈퍼클래스가 super>>readObjectFrom:info:를 이용해 위임하도록 시킨다.
RecordFieldFormat>>readObjectFrom: dataStream into: aValueModel
| recordStream |
recordStream :=
(dataStream upTo: recordDelimiter) readStream.
super readObjectFrom: recordStream into: aValueModel
Decorator는 필드로부터의 값을 최종 스몰토크 객체로 매핑하도록 도와준다. 기본(default)에 따라 결과 객체는 Array가 되므로, 각 값은 IndexedAdaptor에 의해 (ValueModel의 종류) 매핑된다. 이러한 기본은 FieldFormatDescription>>adaptorForIndex:andSubjectChannel:에서 구현된다 (앞 페이지에 나타낸 바와 같이). AspectFieldFormat은 도메인 객체 측면에 필드를 매핑한다. 이것은 IndexedAdaptor 대신 AspectAdaptor를 리턴하기 위해 adaptorForIndex:andSubjectChannel:을 오버라이드한다. IgnoreFieldFormat은 필드의 값을 비트 버킷(bit bucket)으로 매핑하여 ValueHolder를 사용하도록 한다. AspectFieldFormat과 IgnoreFieldFormat은 데코레이터로 구현되므로 잎과 복합 필드의 매핑에 사용할 수 있다.
AspectFieldFormat>>adaptorForIndex: anInteger
andSubjectChannel: aValueModel
^(AspectAdaptor subjectChannel: aValueModel)
forAspect: aspect
IgnoreFieldFormat>>adaptorForIndex: anInteger
andSubjectChannel: aValueModel
^ValueHolder new
따라서 어떤 유형의 필드 또는 필드 그룹도 하나의 레코드로 처리할 수 있다. 필드는 한 측면에 매핑되기도 하고 무시할 수도 있다. 예를 들자면, 필드 집합을 레코드로 그룹화하여 한 측면에 매핑하기 위해 데코레이터를 중첩시킬 수 있다. 한 측면에 필드의 매핑과 무시를 동시에 할 수는 없으므로 AspectFieldFormat과 IgnoreFieldFormat에 있는 코드는 두 데코레이터가 단일 필드에 적용되지 못하도록 방지한다.
알려진 스몰토크 사용예
Wrapper
비주얼웍스의 Wrapper 계층구조를 위에서 살펴보았다. Wrapper 서브클래스는 Decorator로서 VisualComponent에 추가할 수 있다.
ValueModels
위에서 논한 바와 같이 비주얼억스의 여러 ValueModel 클래스는 데코레이터이다.
Stream 데코레이터
비주얼웍스의 BOSSTransporter는 Stream 데코레이터이다. 파일 리더 프레임워크의 FormattedStream 또한 Stream 데코레이터이다. 두 예제 모두 앞서 언급하였다.
FieldFormatDecorator
FieldFormatDecorator는 파일 리더 프레임워크에서 사용되는 FieldFormatDescription 데코레이터이다. 이는 예제 코드 단락에서 논하고 있다.
SyntheticFont
SyntheticFont는 비주얼웍스에 사용되는 ImplementationFont 계층구조의 데코레이터이다. ImplementationFont는 스몰토크 폰트를 운영체제에 장착된 폰트 중 하나로 매핑한다. (이는 Adapter (105) 패턴이다.) Syntheticfont는 플랫폼 폰트에서 지원하지 않을지도 모르는 속성을 폰트에 추가한다. 예를 들어, SyntheticFont는 취소선 설정을 가진다. 취소선이 켜져 있으면 SyntheticFont는 ImplementationFont로 먼저 문자를 그린 후에 문자를 통과하는 선을 그리는데, 다음과 같은 모습이다: 취소선의 사용 예.
관련 패턴
Decorator 패턴 대 Adapter 패턴
Decorator와 Adapter (105) 패턴은 자주 혼동되곤 한다. 두 패턴 모두 다른 객체를 변경하기 위해 특정 객체를 감싸기 때문에 “래퍼(Wrappers)”로 알려지기도 한다. 하지만 Decorator는 자신의 Component의 인터페이스를 보호한다; Adapter는 자신의 Adaptee의 인터페이스를 클라이언트가 예상하는 인터페이스로 전환한다. Decorator는 그 Component로 행위를 추가하거나 변경시킨다; 그 외에는 가치가 없다. Adapter는 그의 Adaptee로 행위를 추가하거나 변경할 수 있지만 그것의 인터페이스를 전환하는 것이 주요 목적이다. Adapter와 Adaptee의 인터페이스가 동일할 경우에는 쓸모가 없다. Decorator는 동일한 인터페이스를 가지기 때문에 쉽게 중첩될 수 있으나 Adapter를 그렇지 않을 뿐더러 대부분의 경우 전혀 합당하지 않다.
Decorator 패턴 대Proxy 패턴
Decorator는 Proxy (23) 패턴과 혼동되기도 한다. Proxy는 그 Subject의 인터페이스를 보존하면서 Subject로의 접근을 통제한다. Proxy는 주로 그 Subject의 행위를 이용 가능 또는 이용 불가하게 만드는 경우를 제외하곤 Subject의 행위를 변경하지 않는다. 다양한 Decorator 서브클래스는 추가할 수 있는 행위가 다양함을 의미한다. 반면 다양한 Proxy 서브클래스는 접근을 통제하는 방법이 다양함을 의미한다.
예를 들어, CachedImage는 비주얼웍스의 PixelArray/Image 계층구조에 있는 클래스로서 Decorator 처럼 보이지만 사실은 Proxy이다. 이것은 Image를 더 효율적이지만 덜 유연한 형태인 Pixmap로 캐싱함으로써 이미지를 감싼다. CachedImage가 Image의 서브클래스로 구현되는 이유는 Image가 수많은 구체적 서브클래스를 가진 추상적 클래스이기 때문이다. CachedImage를 래퍼로 구현함으로써 어떠한 Image 서브클래스 주위든 감쌀 수 있다.
하지만 CachedImage는 행위를 추가하지 않기 때문에 Decorator로 볼 수 없다; 단순히 효율성을 증대시킬 뿐이다. Decorator 행위와 달리 매번이 아니라 CachedImage를 처음 사용할 때에만 캐싱의 초기화가 호출된다. 가장 중요한 것은 CachedImage가 can-be-nested (중첩 가능) 검사를 할 수 없다는 점이다: 다른 CachedImage 주위에 있는 CachedImage를 감싸는 것은 도움이 되지 않는다. 따라서 CachedImage는 Proxy로서, Image의 사용하는 대신 좀 더 효율적인 Pixmap로 사용을 전환한다.
Decorator, Composite, 객체지향 재귀
Decorator와 Composite (137)는 주로 동일한 계층구조에서 함께 사용된다. 두 패턴 모두 그 Component의 인터페이스를 클라이언트가 구조 내 어떤 노드와도 함께 사용할 수 있는 핵심 인터페이스로 제한할 것을 요구한다. 따라서 그 타입의 인터페이스를 한 패턴으로 제한하면 나머지 패턴을 구현하기가 쉽다. 광범위한 Component 인터페이스를 단순화시킨 핵심 인터페이스로 압축시키기란 힘들다. 하지만 우선 성공하고 나면 Decorator와 Component 중 하나를 쉽게 적용할 수 있다.
Decorator는 객체지향 재귀를 (Chain of Responsibility (225) 참조) 통해 자신의 Component와 의사소통을 한다. 구체적 컴포넌트로 끝나는 일련의 중첩된 데코레이터들은 연결고리를 형성한다. 이러한 연결고리에서 상위 데코레이터로 메시지가 전송되면 각 데코레이터는 메시지를 처리할 것인지 아니면 그 컴포넌트로 전달할 것인지, 아니면 둘 다 행할 것인지 결정한다. 메시지가 구체적 컴포넌트에 도달하고 나면 그곳에서 결국 처리된다.
이 세 가지 패턴을 함께 사용하는 데에 관한 자세한 내용은 Chain of Responsibility (225)를 참조한다.
Composite 패턴 대 Decorator 패턴
때때로 트리는 ConcreteComponent 노드뿐 아니라 Composite (137)과 Decorator 노드를 모두 포함하기도 한다. 둘을 어떻게 구별할 수 있을까? 먼저 노드가 가진 컴포넌트의 수를 확인하라. ConcreteComponent 노드는 확실히 컴포넌트가 없으며, Decorator 노드는 정확히 하나의 컴포넌트를, Composite 노드는 다수의 컴포넌트를 가진다. 노드가 다수의 컴포넌트를 가지지만 최근에 하나의 컴포넌트를 가지거나 아예 없는 경우더라도 여전히 다수의 컴포넌트를 가질 수 있으므로 Composite 노드가 된다.
Notes
- ↑ 하위구현은 오버라이드와 동의어다; 슈퍼클래스로부터 상속될 메소드를 구현하는 것이다. 서브클래스는 두 가지 방법으로 메소드를 하위구현할 수 있다: 완전히 교체하여 상속된 구현을 무시하는 방법과, 슈퍼클래스의 메소드를 확장시켜 super 의사 변수(pseudovariable)를 통해 상속된 구현을 참조하고 그 구현에 추가하는 방식이 있다.