DesignPatternSmalltalkCompanion:Composite

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

COMPOSITE(DP 163)

의도

부분-전체 계층을 나타내기 위해 객체를 트리 구조로 만든다. Composite 패턴은 클라이언트가 개별적 객체와 복합 객체를 모두 동일하게 다루도록 한다.

구조

Dpsc chapter04 Composite 01v02.png

전형적인 Composite 객체구조는 다음과 같은 모습이다:

Dpsc chapter04 Composite 02.png

논의

Composite 패턴의 비결은 두 개의 클래스이다: 하나는 단순 객체를 (또는 Leaf) 나타내고 나머지 하나는 객체 그룹을 나타낸다. 그룹 객체 또는 Composite는 그 행위를 그룹 내 객체로 위임함으로써 Leaf와 같은 역할을 한다. 두 클래스 모두 동일한 핵심 인터페이스를 지원하여 클라이언트로 하여금 교대하여 협력할 수 있게 한다. Composite은 그룹 멤버들이 Leaf 객체와 Composite를 모두 포함할 수 있기 때문에 공통 인터페이스를 기회로 활용한다. 하나의 Composite은 다른 Composite들을 비롯해 다른 객체들을 포함할 수 있으며 마지막 Composite들이 Leaf 이외에 포함하는 것이 없을 때까지 계속된다. 그 결과 Composite와 Leaf 객체의 트리가 만들어진다.

트리는 절차적 프로그래밍에서 가장 흔히 사용되고 강력한 데이터 구조들 중 하나이며 (Sedgewick, 1988, 제 4장), 그 중요성은 객체 프로그래밍에도 적용된다. 어떠한 계층구조적, 구성적 관계든 트리로 모델화되는데 적합하다: 정부 내 다양한 부서와 부처, 다중 처리기 내에 시스템, 다국적 기업의 판매영역 등이 포함된다. 문제는 다음과 같다: 계층구조의 층들이 어떻게 똑같이 행동할까? 트리의 모든 가지를 전체 트리처럼 보이도록 만드는 것은 무엇일까? 동일한 행위여야 재사용이 가능하므로 층과 가지를 다르게 만드는 것이 아니라 동일하게 만드는 것이 문제이다.

트리 구조

아래 그림에서 나타나듯이 트리를 구성하는 방법에는 3가지가 있다: 상향식, 하향식, 이중연결식. 노드와 노드들 간 관계는 세 가지 구조 모두 동일하다. 그러나 화살표의 방향이 서로 다르게 구성되어 있다. 상향식 트리에서 각 가지(branch)는 그의 자식(children)을 모두 가리키기 때문에 클라이언트는 가지에게 자식을 요청할 수 있지만 노드는 그 부모(parent)를 알지 못한다. 하향식 트리의 경우, 각 노드가 부모를 가리키지만 가지는 자식을 알지 못한다. 이중연결 트리에서는 각 노드가 부모와 자식을 모두 알고 있다. 화살표 방향은 트리에서 메시지 방향을 결정한다: 아래, 위, 양방향. (Chain of Responsibility (225) 참조)

Dpsc chapter04 Composite 03.png

Composite 패턴은 상방향 트리와 아래로 이동하는 메시지를 정의한다.

윈도우 트리

가장 잘 알려져 있으나 트리 구조처럼 보이지 않는 예제로 윈도잉 시스템의 윈도우 창을 들 수 있다. 윈도우 창은 단순한 직사각형 영역으로 보일지도 모르지만 스몰토크는 내부에 윈도우 창을 트리로 저장한다. 트리에는 3가지 타입의 노드가 있다: 윈도우, 하위뷰, 그리고 위젯이 그것들이다. 아래의 인스턴스 다이어그램은 단순한 윈도우 창의 객체 구조를 나타낸다:

Dpsc chapter04 Composite 04.png

윈도우 창 자체는 트리의 루트, 가지 노드로 구성된 윈도우의 하위뷰들과 그들의 하위뷰들, 마지막으로는 잎(leaf) 노드인 위젯으로 구성된다. 트리의 각 노드는 그것의 부모 노드를 컨테이너라고 부르고 그것의 자식 노드는 컴포넌트라고 부른다. 각 노드가 정확히 하나의 부모를 가지기 때문에 (정의상 부모가 없는 루트는 제외) 그래프가 아니라 트리가 된다.

3가지 종류의 노드는 컴포넌트 수를 보면 확인하기가 쉽다. 윈도우 창 노드는 하나의 컴포넌트만 가진다. 위젯은 컴포넌트가 없기 때문에 잎 노드이다. 하위뷰 노드는 다수의 컴포넌트를 가지기 때문에 가지 노드이다. 하위뷰는 Composite 패턴에서 Composite에 해당한다.

Composite와 Leaf 클래스는 그들의 공통된 컴포넌트 슈퍼클래스가 정의한 핵심 인터페이스를 지원한다. 따라서 트리 내 노드의 클라이언트는 가지 노드와 잎 노드를 구별할 필요가 없다.

이제 비주얼웍스의 VisualComponent 계층구조에 있는 메인 클래스를 살펴보자. 컴포넌트 클래스는 VisualComponent이고, CompositePartSimpleComponent는 Composite 클래스, Leaf 클래스는 DependentPark와 SimpleComponent이다. 여기서 Wrapper가 흥미롭다; 사실상 Decorator (161) 패턴에서 Decorator 클래스이지만 다음 페이지에서 설명하는 바와 같이 Composite 클래스에서는 Leaf 클래스가 된다.

Dpsc chapter04 Composite 05.png

이 VisualComponent 클래스들은 Composite와 Decorator 패턴들의 참가자들이다.

패턴 참가자 비주얼웍스 클래스
Composite와 Decorator 패턴 Component VisualComponent
Composite와 Decorator 패턴 Leaf/ConcreteComponent VisualPart 서브클래스들
Composite 패턴 Composite CompositePart
Decorator 패턴 Decorator Wrapper

다른 방언dialects에서도 Composite 패턴을 통합시키는 이와 유사한 시각적 클래스를 구현한다:

패턴 참가자 비주얼 스몰토크 클래스
Composite와 Decorator 패턴 Component SubPane
Composite와 Decorator 패턴 Leaf/ConcreteComponent SubPane 서브클래스들
Composite 패턴 Composite GroupPane
Decorator 패턴 Decorator 없음


패턴 참가자 IBM 스몰토크 클래스
Composite와 Decorator 패턴 Component CwBasicWidget
Composite와 Decorator 패턴 Leaf/ConcreteComponent CwPrimitive 서브클래스들
Composite 패턴 Composite CwComposite
Decorator 패턴 Decorator 없음

Composite와 Leaf는 동일한 핵심 인터페이스를 가지며 이 인터페이스로 인해 다형적으로 작동할 수 있다. 예를 들자면, 비주얼웍스 내 어떠한 시각 자료든 그것이 선호되는 경계선을 알고 있다. 잎 비주얼은 그것이 얼마나 커지고 싶어 하는지를 안다. 복합 비주얼은 그것의 컴포넌트의 경계로부터 자신의 경계를 계산한다; 따라서 선호되는 경계를 결정하여 하나의 직사각형으로 합친다. 또 다른 예로, 모든 비주얼은 스스로를 그리는 방법을 안다는 사실을 들 수 있다. 잎 비주얼은 스스로를 그냥 그리면 된다. 대부분 복합 비주얼은 그의 컴포넌트들에게 자신을 그려달라고 말하기만 하면 된다.

따라서 단일 노드부터 전체 트리까지 비주얼 트리 중 어떠한 가지도 단일 비주얼로 취급될 수 있고 어떻게 행동할지 들을 수 있다. 행위가 어떻게 생성되는지는 가지 구조의 함수이지만 클라이언트에 의해 숨겨져 있다.

비주얼 스몰토크와 IBM 스몰토크는 그래픽 윈도우 창을 비슷한 방식으로 구현한다. 그들의 클래스 또한 위의 표에 열거하였다. 복합 비주얼 클래스는 비주얼 스몰토크에선 GroupPane, IBM 스몰토크에선 CwComposite이다. 두 비주얼 계층구조 모두 Composite 패턴의 예가 된다.

중첩된 Composite

Composite 패턴의 주요 이점은 Composite을 이용해 Leaf를 그루핑시킬 수 있다는 점뿐만 아니라 Composite을 이용해 다른 Composite들 또한 그루핑시킬 수 있다는 점이다. 다시 말해, Composite는 또 다른 Composite들을 포함하는 Composite를 포함할 수 있는데, 이러한 관계는 모든 가지가 Leaf로 끝날 때까지 지속된다. 이는 재귀적 구조를 형성시켜 트리가 필요한 만큼의 계층 수를 가질 수 있으며, 전체 트리와 트리 가지의 차이가 분명하지 않게 된다. 구조 단락에 그려진 객체 다이어그램이 (p. 137) 이를 잘 설명해준다.

Composite 패턴의 모든 예제들이 이러한 유형의 무제한 중첩을 뒷받침하는 것은 아니다. 패턴을 엄격하게 해석한다면 무제한 중첩을 뒷받침하지 않는 한 예제에 속하지 않는다. 하지만 패턴의 주요 원칙 중 하나는 객체와 객체의 집합을 동일하게 취급하라는 것이며, 이러한 특징을 가진 예제라면 패턴의 모든 특징을 나타내지 않는다 하더라도 패턴을 더 잘 이해할 수 있을 것이다.

제한된 자식 타입

일반적으로 Composite는 Composite와 Leaf 노드 둘 다 포함할 수 있다. 하지만 일부 분야에서는 이를 제한한다. 예를 들어 컴퓨터를 칩을 포함하는 회로판 복합객체로 모델화시킬 수 있다 (다음 페이지 참조):

Dpsc chapter04 Composite 06v2.png

Chip은 Leaf 클래스이고, Computer와 CircuitBoard는 Composite 클래스이다. 최상위 Composite 클래스는 어떠한 컴포넌트도 포함할 수 있지만 그 서브클래스는 조금 더 제한적이다.

Composite 패턴을 제한 없이 사용하는 애플리케이션은 컴퓨터가 다른 컴퓨터를 포함시키고, 회로판이 컴퓨터를 포함시키며, 컴퓨터가 회로판에 속하지 않는 칩이나 회로판을 포함시킬 수 있도록 해준다 (따라서 어떤 칩이 장착되었는지 궁금할 것이다).

Composite 패턴이 허용할지 모르는 무효한 (invalid) 구성을 예방하기 위해 Computer와 CircuitBoard의 구현은 이를 예방하는 코드를 포함하고 있어야 한다. 이러한 클래스들은 Computer에 CircuitBoard는 포함하되 Chip이나 다른 Computer는 포함하지 않도록 보장하고, CircuitBoard에 Chip은 포함하되 다른 CircuitBoard나 Computer는 포함하지 않도록 보장해야 한다.

따라서 Composite 패턴은 도메인 측면에서 볼 때 의미가 통하지 않더라도 어떠한 트리 구조든 허용하는 것이 일반적이다. 도메인에 유효 트리가 포함된 경우 그러한 제약은 Composite 트래와 그 서브클래스에 부호화되어야 한다.

Composite 패턴이 이러한 예제에 정말 필요할까? 패턴의 한 가지 주요 이점이라고 하면 도메인과 달리 자식을 임의로 중첩되도록 허용한다는 점인데, 왜 굳이 이 패턴을 사용할까? 우리가 모델링하는 컴퓨터는 컴포넌트를 어떻게 중첩하는지는 제한하지 않기 때문에 제약을 좀 더 정확히 표현하자면 다음과 같다:

Dpsc chapter04 Composite 07.png

어떤 모델이 더 괜찮을까? Composite 패턴을 사용 시 이점은, Computer와 CircuitBoard의 복합적 특징을 CompositeComponent에 한 번 정의하고 구현할 수 있다는 점이다. 이 패턴을 사용하지 않을 경우, 그 boards를 처리하기 위해 Computer에 구현된 코드 대다수가 chips의 처리를 위해 CircuitBoard에도 구현되어야 한다. 새로운 요구사항에서 예를 들어 InputOutputDevice와 같은 또 다른 복합 부품을 도입한 경우, 그 클래스는 복합 행위를 다시 복제해야 할 것이다. 반면 각 Composite 클래스가 코드를 복제하고 있는 한 코드는 제약을 더 쉽게 처리할 수 있다; 예를 들어, 인스턴스 변수를 좀 더 일반적인 components 대신 boards와 chips로 부를 수 있다.

자신의 결정에 따라 다음과 같은 질문이 생길 것이다: Computer와 CircuitBoard는 얼마나 비슷하게 행동할까? 완전히 다른 객체인가 아니면 작은 컴퓨터 부품의 결합체에 불과한가? 답이 무엇이든 이것은 3개의 층으로 구성된 트리 구성이다. 각 층은 비슷하기 때문에 설계 상 비슷한 점을 포착하고 클라이언트 객체가 각 종류의 노드를 비슷하게 취급하도록 하며 코드 중복을 피하기 위해 Composite 패턴을 사용한다.

제한된 자식 수

일반적으로 Composite는 원하는 만큼의 자식을 가질 수 있다. 컴포넌트 노드는 보통 자식의 수를 신경쓰지 않는다. 대부분 도메인은 이를 허용하며 심지어 이를 예상하기도 한다. 예를 들어, 복합 비주얼은 얼마나 많은 컴포넌트 비주얼을 포함하고 있는지 신경쓰지 않는다. 컴퓨터는 그것이 포함하는 회로판 수를 제한하지 않으며, 회로판이 포함할 수 있는 칩의 수도 마찬가지로 제한이 없다. Composite의 구현은 자식에 대해 Collection을 사용함으로써 이를 뒷받침한다.

그러나 일부 도메인은 가지가 가질 수 있는 자식 노드의 수를 제한한다. 예를 들자면, 이진 트리(binary tree) 노드는 자식 노드를 무제한으로 가질 수 없다ㅡ최대한이 2개이다. 2개로 번식하는 단일세포 조직을 모델화하려면 그 조직, 자식에 대한 그의 쌍, 자식의 쌍 등을을 추적할 것이다. 따라서 각 조직체는 자식 수를 무제한이 아니라 2개로 제한된다.

Composite 패턴은 무제한 수의 자식뿐 아니라 가지가 자식 수를 고정시키는 것도 허용한다. [디자인 패턴] 편에서는 Collection에 속하는 children 변수를 이용해 무제한 자식 수의 사례를 구현하는 방법을 나타내고 있다. 제한된 자식 수를 지원하려면 각 잠재적 자식에 대해 구분된 변수를 구현하라. Composite에 두 개의 자식이 있는 경우 leftChild와 rightChild 또는 mother과 father과 같이 두 개의 변수를 구현해야 한다. Composite은 하나 또는 그 이상의 자식이 명시되지 않은 사례도 처리할 수 있을 것이다.

Composite 오퍼레이션은 자식 수가 제한된 Composite에서 다르게 구현될 것이다. 자식 수가 무제한인 경우는 자식이 Collection에 저장된다; 그리고 이 메서드는 do: 와 같은 메시지를 사용하여 이들을 반복한다. (Iterator (273) 참조). 제한된 자식 수를 가진 경우 Composite 오퍼레이션은 각 자식에게 명시적으로 메시지를 전송한다. 오퍼레이션이 결과를 가지면 Composite 오퍼레이션은 결과를 수집하여 통합한다. 예를 들어, 무제한 자식 수를 가질 수 있는 Composite 오퍼레이션에서 오퍼레이션을 위한 일반 코드는 다음과 같을 것이다:

Composite>>operation
	self children do: [ :child | child operation]

여기에 비슷한 동작을 하는 세개의 자식을 가지는 Composite가 있다:

Composite>>operation
	"Assumes none of the children is nil."
	self firstChild operation.
	self secondChild operation.
	self thirdChild operation

추상적 잎 클래스와 복합 클래스

Abstract Leaf and Composite Classes

패턴의 예제들은 때때로 많은 공통 구현을 공유하는 수 많은 구체적 Leaf 클래스를 가지곤 한다. 예를 들자면, 트리 노드는 그 자식의 목록을 리턴할 수 있는 것이 보통이다. Composite은 그 컴포넌트를 리턴한다; 그것이 어떤 종류든 Leaf는 항상 빈 집합체를 리턴한다.

이 공통 행위를 어디에 구현할 것인지가 문제가 된다. Component에 행위를 구현하는 것은 그것이 모든 Leaf 클래스뿐만 아니라 Composite 클래스에 의해서도 상속될 것을 의미한다. 하지만 Leaf 클래스로 하향이동시키는 것은 동일한 코드를 여러 번 복제할 필요가 있음을 나타낸다.

이보다 더 나은 해결책은 모든 구체적 Leaf 클래스에 대한 공통된 Leaf 슈퍼클래스이다. 이 슈퍼클래스는 주로 추상적 클래스이며 모든 구체적 Leaf 클래스에 공통된 코드를 구현하지만 이것은 Composite 클래스에는 적절하지 않다. 이와 유사한 방식으로, 복합 Composite 슈퍼클래스는 components 인스턴스 변수와 같은 구체적 Composite 클래스에 의해서만 상속되어야 하는 코드를 구현하는 데에 유용하다.

Composite과 Leaf를 추상 클래스로 구현하게 되면 구조 다이어그램이 다음과 같이 바뀐다:

Dpsc chapter04 Composite 08.png

Composite는 서브클래스이다

초보 설계자가 Composite 패턴을 이용 시 경험할 수 있는 혼란스러운 점이 한 가지 있다면 Composite 클래스가 Component 클래스의 서브클래스라는 점이다. Composite 인스턴스는 그것이 포함하고 있는 Component들의 부모이기 때문에 직관에 어긋나는 듯 보이기도 한다. Composite 패턴의 첫 장에 소개된 구조 객체 다이어그램을 보면 Composite가 Component를 포함하는 일반적 트리 구조를 볼 수 있다. 각 Composite는 그 Component들의 부모이다. 인스턴스들 간 부모-자식 관계는 인스턴스를 구현하는 클래스들 간 관계와 유사함을 나타낼 수 있다ㅡ즉 Component 클래스가 Composite 클래스의 서브클래스여야 한다. 하지만 Composite 클래스가 충족시켜야 하는 인터페이스를 정의하는 것은 Component이어야 하며 그 반대가 아니다. 따라서 Component 클래스는 Composite 클래스의 슈퍼클래스가 된다. 클래스 상속을 인스턴스들 간 부분-전체 관계와 혼동하지 않도록 한다.

Component 슈퍼클래스가 없는 경우

스몰토크에서는 때때로 하나의 Composite 클래스와 그의 Leaf 클래스가 동일한 계층구조에 위치하지 않는 경우도 있다. 대신 Composite 클래스가 Collection 계층도의 일부가 되는 경우도 있다. 이렇게 구현된 예제는 Composite 패턴을 사용하려는 의도를 가지지만 설계와 구현을 성공적으로 수행하지 못한다. 이러한 예제는 공통된 Component 슈퍼클래스가 없기 때문에 두 클래스가 지지해야 하는 인터페이스에 대한 명확한 정의가 없다. 하지만 이것은 C++에서는 꿈도 꾸지 못하는 일인데 그 이유는 컴파일러의 강력한 타이핑 때문이다. Java의 경우 두 클래스가 구현하는 공통 인터페이스를 이용해 성공적으로 실행할 수 있으나 두 클래스가 공통된 추상 슈퍼클래스에 구현되는 편이 아마도 더 나은 코드를 복제할 것이다. 이러한 기법은 언어의 동적 타이핑 덕분에 스몰토크에서도 가능하지만 유연성을 남용하는 것은 부적절하다. 여러분의 코드에 Composite 패턴을 구현 시에는 이러한 기법을 피하도록 한다.

비주얼웍스에 사용되는 FontDescriptionBundle 또한 두 개의 구분된 계층구조에 구현되는 Composite 패턴의 예가 된다. 이름이 암시하는 바와 같이 FontDescriptionBundle은 FontDescription들의 집합체이다. FontDescription은 런타임 플랫폼에서 폰트에 묶이게 될 폰트의 플랫폼 독립적 설명을 나타낸다. FontDescription은 Proxy (213)에 속한다. FontDescriptionBundle은 클라이언트로 하여금 한 번에 다수의 FontDescription을 위한 플랫폼을 검색하도록 해준다.

FontDescriptionBundle은 다수의 FontDescription을 한데 모으고 두 클래스가 공통된 인터페이스를 구현하므로 확실히 복합 객체FontDescription가 된다. 예를 들어, FontDescription>>findMatchOn:allowance: 는 플랫폼의 폰트에 적절한 일치대상이 있는지 검색한다. FontDescriptionBundle은 어떠한 폰트든 그에 적절한 매치를 찾기 위해 동일한 메시지를 구현한다. FontDescriptionBundle을 이용할 경우 이러한 오퍼레이션은 실패하는 경우가 적은데 이것은 번들의 설명 중 하나라도 일치하는 폰트를 수락할 것이기 때문이다. 반면 이 트리는 2층 깊이에 그치기 때문에 Composite의 예로 들기엔 약하다.

두 개의 클래스가 공통된 인터페이스와 목적을 가지기 때문에 동일한 계층도에 상주할 것으로 예상되지만 사실 그렇지 않다. FontDescription은 Object의 서브클래스인 반면 FontDescriptionBundle은 OrderedCollection의 서브클래스이다. 클래스에 공통 슈퍼클래스가 없는 경우 두 클래스에 다형적으로 새로운 행위를 추가하기가 힘들어진다. FontDescription에 새 메시지를 구현하고자 하는 개발자는 FontDescriptionBundleㅡ완전히 구분된 계층구조에 위치한ㅡ에도 메시지를 구현해줘야 한다는 점을 명심한다. 이 클래스들이 다형적으로 행동하도록 만드는 것은 개발자가 확장할 수 있는 공통 슈퍼클래스가 있는 경우 훨씬 더 간단해짐과 동시 오류발생 또한 줄어들 것이다.

Component의 핵심 인터페이스

[디자인 패턴]에서는 Component 클래스가 일반 오퍼레이션과 Composite 특정적 오퍼레이션을 모두 가져야 한다고 제시한다. 이는 Component가 (Composite가 아니라) Add(), Remove(), GetChild()와 같은 메시지를 선언해야 함을 보여주는 구조 다이어그램에도 반영되어 있다. 구현 고려사항 3부터 5까지 (DP 167-169)는 안전과 투명성 간 균형으로 이를 논한다.

C++에 지나치게 편중된 [디자인 패턴] 편에서는 투명성을 중시하여 C++에서의 타입 검사(type checking)가 너무 부담스럽지 않도록 한다. Component가 Composite 특정적 메시지를 선언하지 않았다면 클라이언트는 Composite와 Component를 동일하게 처리하지 못할 것이다. 첫째, 클라이언트는 Composite 특정적 메시지를 전송하기 전에 Component를 Composite로 보낼 것이다. 스몰토크에서는 이러한 타이핑 제약이 없기 때문에 투명성보다는 안전성을 지지한다. Composite만 이러한 메시지를 유용하게 구현할 수 있으므로 Component에 추가되지 않으며, 이에 따라 Leaf도 이러한 메시지를 구현하지 않는다.

[디자인 패턴]에서 뒷받침하는 투명성은 문제가 많다. 올바른 질문은 다음과 같다: Component가 이러한 Composite 메시지들을 선언할 예정이라면 Leaf는 이를 어떻게 구현해야 하는가? 어떠한 것도 하지 않고 nil을 리턴하도록 구현할 수도 있고, 실패하여 오류를 리턴하도록 구현할 수도 있다. 안정성을 택할 경우, 클래스는 의미 있게 구현할 수 없는 메시지는 이해할 수 없어야 한다고 말한다. 투명성을 택할 경우, 모든 Component 슈퍼클래스들은 동일한 인터페이스를 구현하여 슈퍼클래스를 서로 대체하여 사용할 수 있어야 한다고 말한다.

스몰토크는 이 딜레마를 핵심 인터페이스라 불리는 개념을 이용해 해결한다. 핵심 인터페이스는 객체의 모든 책임을 구현하는데 충분한 행위를 구현하지는 않지만 주요 협력을 지원하기엔 충분하다. 예를 들어, Collection 클래스인 Set, OrderedCollection, SortedCollection는, 제안된 ANSI 스몰토크 표준에서 “확장 가능한”과 축소 가능한”이라 부르는 두 프로토콜을 공유한다 (X3J20, 1996). 이는 이 클래스들이 add: 나 move: 와 같은 메시지를 이해할 수 있음을 의미한다. 이러한 핵심 인터페이스는 클래스를 서로 교환하여 사용 가능하게 해준다는 점에서 중요하지만, Array를 대신 사용할 수는 없었다. 그렇다고 해서 클래스의 전체(full) 인터페이스가 동일한 것은 아니다. OrderedCollection은 at:put:을 이해하지만 Set또는 SortedCollection은 이해하지 못한다. SortedCollection은 그 속성을 설정하기 위해 sortBlock:을 구현하지만, 오퍼레이션 RoderedCollection과 Set는 지원하지 않는다. 하지만 Collection 클래스들은 너무나도 비슷하게 작동하여, 핵심이 되는 추가/제거 인터페이스를 이용하는 클라이언트가 집합체의 클래스 또는 전체 인터페이스를 알지 못해도 되는 정도이다.

핵심 인터페이스는 당신이 객체의 프로토콜을 아무렇게나 대하도록 두지만 강 타이핑 언어에서 훨씬 더 까다롭게 만든다. C++나 Java에서는 클래스의 인터페이스가 미리 선언되어야 하며, 컴파일러는 객체에 수신된 메시지를 객체가 모두 구현하도록 보장한다. 모든 서브클래스가 이해하는 모든 메시지는 인터페이스의 일부가 되어야 하는데, 이는 모든 서브클래스들이 그 메시지를 구현해야 함을 의미한다. 스몰토크는 동적으로 타이핑되어 있으므로 컴파일러는 객체가 자신에게 수신된 메시지를 이해하는지 확인할 방법이 없다. 핵심 인터페이스는 모든 서브클래스가 이해하는 메시지들을 포함하는 일만 한다. 하지만 런타임 시 객체가 메시지를 이해하지 못하는 경우 message-not-understood 오류가 발생한다.

Composite 패턴에서 한 가지 비결은 Composite와 Leaf 객체를 Component로서 교대하여 취급하는 것이다. 강력한 타이핑 언어에서 이를 실행하려면 모든 서브클래스가 구현할 메시지를 Component가 선언해야 한다. 다시 말해 addChild:, removeChild:, getChildAt:과 같은 메시지들을 선언한 후 Leaf가 이를 구현하도록 강요한다. 스몰토크와 같이 동적으로 타이핑된 언어에서 슈퍼클래스는 (Component) 모든 서브클래스가 구현할 메시지만 선언하면 된다. 균형을 맞추기 위해, Composite와 Leaf들을 서로 대신해 취급하고자 하는 클라이언트는 그들이 공유하는 핵심 인터페이스만 사용해야 한다. Leaf들은 그들이 어떤 방식으로 구현하든 추가나 제거 작업은 처리하지 않기 때문에 컴포넌트를 추가 또는 제거하길 원하는 클라이언트는 수신자가 Composite인지 아닌지 여부를 알아야 하는 의무를 피할 길이 없다. 그러나 코드가 컴포넌트를 추가하거나 제거하지 않는 이상 Composite와 Leaf를 교대하여 취급할 수 있어야 한다.

이에 따라 스몰토크에서 사용되는 Composite 패턴의 구조 다이어그램은 [디자인 패턴]에서 나타내는 구조와는 다르다. [디자인 패턴] 편의 Component는 Add(), Remove(), GetChild()와 같은 Composite 메시지를 구현한다. 스몰토크에서 패턴을 구현 시 Component가 주로 이러한 메시지를 구현하지 않는 이유는 모든 서브클래스에 적절한 메시지가 아니기 때문이다. 스몰토크에서 Component는 operation과 같이 모든 서브클래스에 적절한 메시지만 정의한다. 이러한 메시지들은 클라이언트가 투명하게 사용할 수 있는 유일한 메시지이다. Composite 메시지를 사용하기 위해 클라이언트는 먼저 그것이 아무 Component가 아니라 Composite와 협력하고 있는지를 결정해야 한다.

핵심 인터페이스 구현하기

Composite 패턴을 위한 핵심 인터페이스를 구현하기 위해서는 먼저 템플릿 메서드 (355) 패턴으로 Component 클래스에 인터페이스를 구현해야 한다. 이러한 구현은 인터페이스의 메시지를 소수의 커널 메시지와 관련해 구현할 것이다. Composite과 Leaf 클래스 모두 커널 메시지를 구현해야 하므로 Component는 가능한 한 적은 수의 커널 메시지를 사용해야 한다.

둘째, 커널 메시지의 구현을 Component 클래스에게 미룬다. 클래스는 이러한 메시지를 subclassResponsibility 또는 implementedBySubclass로 구현함으로써 서브클래스로 메시지의 구현을 미룬다.

셋째, Composite와 Leaf 서브클래스에 커널 메시지를 구현하라. Leaf 클래스에서는 메시지가 그 행위만 수행할 뿐이다. Composite 클래스에서는 메시지가 각 자식에게 메시지를 전달하고 그들의 행위를 한데 모은다.

넷째, Composite나 Leaf의 어떤 서브클래스에서든 핵심 인터페이스의 확장을 피하라. 이 서브클래스들은 확장된 인터페이스를 가질 수 있지만 Component의 인터페이스의 부분이 아니기 때문에 클라이언트 객체는 확장된 프로토콜을 다형적으로 사용할 수 없을 것이다. 따라서 확장된 인터페이스를 사용하기 위해서는 클라이언트가 먼저 수신자 타입을 결정해야 한다. 이는 Composite 패턴의 목적과 상충된다.

위의 단계 중 네 번째 단계가 특히 까다로운데, 그 이유는 모든 서브클래스가 본질적으로 Component클래스와 동일한 인터페이스를 가져야 하기 때문이다. 따라서 Component 클래스는 그것이 필요로 할뿐 아니라 모든 서브클래스에 만족할 수 있는 광범위한 인터페이스를 선언해야 하는 것이다. 실습에서 서브클래스가 추가하는 메시지는 인스턴스 생성 메시지들이다. 클라이언트는 인스턴스를 생성할 때 인스턴스의 구체적 클래스를 알고 Component의 인스턴스 대신 확장된 인스턴스를 사용한다.

Composite 클래스를 예로 들면, addChild: 메시지가 확장부이지만 이것이 Composite를 생성했다는 것을 클라이언트가 알게 되면 이는 인스턴스 생성의 일부로 사용된다. 이와 유사한 방식으로, 클라이언트는 노드가 제거하고 싶어하는 자식이 있는지 검사하기 때문에 removeChild: 메시지를 전송하기 전에 노드가 Composite임을 인지한다. 노드가 Composite가 아닐 경우 애초에 자식을 포함할 수 없다.

필드 양식 설명 트리

드물게 사용되는 Composite 패턴의 또 다른 예로 레코드위주의 플랫 파일을 읽기 위한 프레임워크를 들 수 있다. 각 레코드는 필드로 구성되며, 각 필드는 문자열이나 숫자와 같이 단순한 유형이다. 각 파일 레코드는 절차형 언어에 정의된 레코드 구성과 일치한다. 객체지향 언어에서 레코드 구성은 클래스에 해당하며, 파일 내 레코드 자료의 각 집합은 클래스의 인스턴스에 해당한다.

레코드 위주의 플랫 파일을 읽기 위한 프레임워크는 최소한 2개의 클래스, Field와 Record를 필요로 하는데, Record는 Field의 집합이 된다. 이 두 클래스는 Composite 패턴을 이용해 구현될 때 훨씬 더 작동하는데, 그 이유는 Composite 패턴이 readFromStream: 과 같은 메시지를 다형적으로 구현하도록 해주기 때문이다. Field는 파일 스트림으로부터 다음 필드를 읽기 위해 그것을 구현하고, Record는 스트림으로부터 그것의 필드를 읽어내기 위해 그것을 구현함과 동시 레코드 구획 문자(Record delimiter)가 있을 시 이를 읽어내기도 한다. 클라이언트는 실제로 읽는 것과 상관없이 단일 필드 또는 전체 레코드를 읽기 위해 동일한 메시지를 사용할 수 있다.

Composite 패턴은 프레임워크가 파일 구조의 중첩(nesting)을 시작하도록 해준다. 한 레코드를 다른 레코드 안에 중첩키는 것은 말이 안 되지만, 공통된 일련의 필드가 다른 공통된 일련의 필드 내에 중첩되는 것은 말이 된다. 이는 기본적으로 한 레코드에 중첩된 하위레코드이다. 예를 들어, Customer에 속하는 필드는 그 Address에 대한 필드를 포함할 수 있다. 이는 Address와 같은 도메인 객체가 Customer와 같은 다른 도메인 객체에 단일 객체로서 중첩되는 것과 일치한다. 따라서 Record는 선택적 레코드 구획 문자가 있는 CompositeField일 뿐이다. 진정한 Composite 클래스는 단일 필드처럼 행동하지만 다수의 Field를 포함하는 CompositeField가 된다. 이는 중첩 객체들이 중첩 필드처럼 Record 내에 보관되도록 해준다. 이 다이어그램은 다음과 같은 클래스를 나타낸다:

Dpsc chapter04 Composite 09.png

Field는 단순한 AbstractField이므로 Stream으로부터 자신을 읽기만 하면 된다. (이 예제는 데이터가 읽히고 나면 이후에 데이터에 무슨 일이 일어나는지는 보여주지 않는다.) CompositeField는 일련의 필드를 포함하고 있으며, Stream으로부터 필드를 하나씩 읽음으로써 자신을 읽는다. Record는 이보다 한 단계 더 나아간다: 레코드로부터 데이터를 얻기 위해 그 다음 레코드 구획 문자만큼 읽는다.

놓친 기회들

때때로 당신은 Composite 패턴을 이용해 구현했다면 더 수월했을 법한 클래스를 발견하곤 한다. 클라이언트가 객체와 객체의 집합체를 구별해야 하는 경우 그 코드는 어색할 뿐더러 경우에 따라 다르게 나타날 것이다. 클라이언트의 구현부는 협력하는 클래스에 Composite 패턴을 적용시킴으로써 단순화될 수 있다. 그리고 나면 클라이언트는 객체와 객체의 집합체를 동일하게 취급할 수 있게 될 것이다. 예를 들어, 비주얼웍스의 SelectionInList는 객체의 목록과 최근 선택된 객체를 모두 추적한다. 이는 ValueModel 두 개를 이용해 구현된다: 하나는 집합체를, 나머지 하나는 집합체에서 선택의 지수를 담당한다. SelectionInList는 ValueModel과 함께 사용되곤 하지만 서로 다른 인터페이스를 가지므로 서로 대체해서 사용할 수는 없다.

ValueModel의 연결 고리가 쉽게 SelectionInList들을 포함할 수 있도록 SelectionInList와 ValueModel은 대체 가능해야 한다. 클래스를 교체 가능하게 만들기 위해서는 동일한 다형적 인터페이스를 가져야만 한다. SelectionInList는 ValueModel과 동일한 인터페스를 가져야 하며, 이로 인해 그 value를 선택 가능해야 한다. 그리고 나면 어떠한 위젯이든 그것이 SelectionInList인지 여부와 관계 없이 ValueModel을 사용할 수 있다.

따라서 어떤 패턴은 SelectionInList와 ValueModel에 적용시켜 다형적으로 만들어야 한다. SelectionInList는 두 개의 ValueModel로 구성되고 ValueMode처럼 행동해야 하기 때문에, Composite 패턴을 적용할 경우 적절한 변형을 유발할 수 있다. 이는 SelectionInList를 ValueModel 계층구조로 이동시키고 그 컴포넌트 ValueModel들을 위임한 후 그 행위를 한데 모음으로써, SelectionInList를 ValueModel 계층구조에 이동시키고 그의 ValueModel 인터페이스를 구현할 것이다.

SelectionInList가 ValueModel 계층도의 일부가 되고 나면, value와 같은 표준 ValueModel 메시지를 구현해야 할 것이다. 여기 이런 메시지가 어떻게 구현되는지 예제를 보이고자 한다.

SelectionInList>>value
	| list selectionIndex |
	list := self listHolder value.
	selectionIndex := self selectionIndexHolder value.
	^selectionIndex = 0
		ifTrue: [nil]
		ifFalse: [list at: selectionIndex]

Composite인 SelectionList가 각 컴포넌트 ValueModel에게 메시지를 위임한 후 결과를 모으는 과정을 통해 어떻게 value를 구현하는지 살펴보라 (이번 사레에서는 at:를 사용). 이것이 일반적인 Composite 행위이다.

구현

[디자인 패턴] (DP 166-170) 편에서 논한 구현 문제 외에도 Composite를 구현 시 고려해야 할 몇 가지 문제가 있다:

  1. Component 슈퍼클래스를 사용하라. Composite 클래스는 공통된 Component 슈퍼클래스를 가진 Leaf 클래스와 동일한 계층도에서 구현되어야 한다ㅡ두 개의 구분된 계층도에 구현되어선 안 된다. Composite와 Leaf의 공통 인터페이스를 정의하는 공통된 Component 슈퍼클래스를 가지는 것이 중요하다. FontDescriptionBundle에 대한 앞선 토론에서 Composite와 Leaf를 동일한 계층구조에서 구현하지 않을 시 발생하는 문제를 소개한 바 있다.
  2. Composite와 Leaf의 추상적 서브클래스의 구현을 고려하라. 이는 Component 계층도를 두 개의 구별된 하위계층구조로 나눌 것이다: Component로 구성된 클래스와 (Composite들) 더 이상 분해되지 않는 클래스로 (Leaf들) 나뉜다.
  3. Composite만이 자식에게 위임한다. Composite 계층구조 내의 클래스가 컴포넌트를 직접 조작하는 것도 귀가 솔깃한 일이지만 이는 Composite 슈퍼클래스에게 남겨두는 것이 좋다. Composite 클래스가 그 컴포넌트로의 위임 이상의 일을 하고 서브클래스가 위임 전후에 행위를 추가하기 위해 이 프로토콜을 확장한다면 계층구조의 코드는 재사용성이 높아지고 더 유연해질 것이다. 이러한 방식으로 계층도를 구현하는 데 대한 더 자세한 정보는 템플릿 메서드를 (355) 참조한다.
  4. Composite들을 중첩시킬 수 있다. Composite은 Leaf뿐만 아니라 다른 Composite도 포함할 수 있다. 프로그래머는 가끔씩 이를 망각하고 Composite의 자식들이 잎이라는 가정을 하여 Composite를 구현하곤 한다. 클라이언트가 처음으로 Composite를 다른 Composite에 자식으로 삽입하고 나면 때때로 그 구현을 잊어버려 상위 Composite가 실패하곤 한다. 이와 관련된 예를 예제 코드 단락의 CompositeAsset>>containSecurity: 구현에서 찾아볼 수 있다.
  5. Composite는 부모 역 포인터(back-pointer)를 설정한다. 트리는 주로 이중으로 연결되어 있어 가지 노드가 그 자식을 가리키고 각 자식은 다시 부모를 가리킨다. 이러한 구조에서는 두 개의 포인터를 동시에 설정할 수 있기 때문에, 하나만 설정하고 나머지는 잊어버리는 일이 없다. 최적의 실행 장소는 Composite>>addChild: 이다. 이 메서드는 Component를 Composite의 자식으로 추가가 가능할 뿐 아니라 Component의 부모를 Composite로 설정할 수도 있다. 그 외의 어떤 코드도 Component의 부모를 설정해서는 안 된다. 이와 비슷하게 Composite>>removeChild: 또한 양방향으로 연결을 끊어야 한다.
  6. Composite는 어떠한 자식 타입이든 포함할 수 있는가? 이는 구현 문제가 아니라 도메인 특정적 문제이다. 설계에서 Composite 객체 내의 객체 타입을 제한하는 경우 Composite의 구현은 유효성 코드를 통해 이러한 제약을 실시해야 한다. 예를 들어, addChild:의 구현부는 자식을 추가하기 전에 자식의 타입을 검증할 필요가 있다.
  7. Composite의 자식 수가 제한되어 있는가? 제한이 되어 있을 경우 각 자식을 저장하기 위해 구분된 이름의 변수를 사용한다. 아니면 무제한으로 자식 수를 저장할 수 있는 단일 변수 Collection을 사용하라. 제한 수가 크고 자식이 Composite 내에서 구분된 역할을 가지는 경우가 아니라면 Collection 변수를 사용하여 addChild: 메서드에 제한을 적용한다. Collection을 통해 반복하거나 이름이 있는 변수를 하나씩 차례로 고려하려면 Composite 오퍼레이션을 구현한다.
  8. Composite가 그 오퍼레이션 메시지를 컴포넌트로 전달하는 방법에는 4가지가 있다:
    • 단순 전달: 모든 자식에게 메시지를 전송하고 그 외 다른 행위를 수행하지 않은 채 결과를 수집한다.
    • 선택 전달: 자식 중 일부에게만 조건적으로 메시지를 전달하고 결과를 수집한다.
    • 확장 전달: 전체 또는 일부 자식에게 메시지를 전달하기 이전이나 전달하는 도중에 다른 행위를 수행하고 그 결과를 수집한다.
    • 오버라이드: 자식에게 메시지를 전달하는 대신 행위를 수행한다; 그 행위가 아무런 일을 하지 않을 수도 있다.

예제 코드

재정 도메인financial donaim으로부터 일반적 예를 하나 살펴보자. 하나의 Account는 Security들로 구성되어 있고, 클라이언트의 Portfolio는 그것의 Account들로 구성되어 있다. Security는 Stock, Bond, 그 외 자산 타입에 해당한다. 클라이언트는 그의 모든 Account에 속한 Security의 값을 모두 합하여 결정되는 Portfolio의 값을 알고 싶어할 것이다. 특별한 Security를 소유하고 있다면 그것에 대해서도 알고 싶어한다. 따라서 이 값은 그의 Portfolio에 포함된 모든 Account 내에서 해당 Security를 검색하여 결정된다.

그의 도메인에 해당하는 기본적인 객체 모델은 다음과 같다:

Dpsc chapter04 Composite 10.png

이러한 객체 모델을 구현하기 위해서는 첫째, Asset을 Component 클래스로 구현한다. Asset은 value와 containsSecurity라는 두 개의 메시지를 선언하며, 그 서브클래스는 다음과 같이 구현될 것이다:

Object subclass: #Asset
	instanceVariableNames: ''
	classVariableNaems: ''
	poolDictionaries: ''

Asset>>value
	"Return the value of this Asset."
	^self subclassResponsibility

Asset>>containsSecurity: aSecurity
	"Answer whether this Asset contains aSecurity."
	^self subclassResponsibility

다음으로 Security를 구현한다. 그 값을 저장하도록 하나의 인스턴스 변수를 필요로 할 것이다. 이는 value와 containsSecurity: 를 구현할 수 있으므로 그렇게 하도록 한다:

Asset subclass: #Security
	instanceVariableNames: 'value'
	classVariableNames: ''
	poolDictionaries: ''

Security>>value
	"See superimplementor."
	^value

Security>>containsSecurity: aSecurity
	"See superimplementor."
	"For a Leaf, we'll say it includes aSecurity
	if it is aSecurity."
	^self = aSecurity

이제 Composite 클래스인 CompositeAsset을 구현하자. 여기서는 그의 자식을 저장할 수 있는 인스턴스 변수 assets와 이 변수로의 접근을 가능하게 하는 메서드가 필요할 것이다. 또한 value와 containSecurity: 를 Composite 오퍼레이션으로 구현하기도 한다:

Asset subclass: #CompositeAsset
	instanceVariableNames: 'assets'
	classVariableNames: ''
	poolDictionaries: ''

CompositeAsset>>assets
	"Return the list of assets."
	^assets

CompositeAsset>>value
	"See superimplementor."
	"Return the sum of the assets."
	^self assets
		inject: 0
		into: [ :sum :asset | sum + asset value]

CompositeAsset>>containsSecurity: aSecurity
	"See superimplementor."
	"See if one of the assets is aSecurity."
	^self assets includes: aSecurity

containsSecurity:의 구현을 자세히 살펴보자. 이 오퍼레이션은 assets을 Security들의 Collection으로 가정한다. 이는 Composite가 Account라면 옳은 가정이지만 Composite가 Portfolio일 경우는 틀린 가정이 된다. 따라서 Portfolio의 경우 이 구현은 false 값을 리턴하지만 이 버그(bug)는 쉽게 찾을 수 있거나 분명히 나타나는 것이 아니다. Composite가 Leaf만 포함하고 있는 것이 아님을 명심하자; 다른 Composite도 포함이 가능하다. 그러므로 자산이 다른 Composite가 될 수도 있다는 가정 하에 containsSecurity:를 구현해야 한다.

CompositeAsset>>containsSecurity: aSecurity
	"See superimplementor."
	"See if one of the assets is aSecurity."
	self assets
		detect: [ :asset | asset containsSecurity: aSecurity]
		ifNone: [^false].
	^true

containsSecurity:의 향상된 구현은 value의 구현과 마찬가지로 객체지향 재귀의 예제이다 (Chain of Responsibility (225) 참조). Composite 내의 구현부가 객체지향 재귀를 사용해야 한다는 점을 주목하라; containsSecurity: 의 기존 구현에서는 includes:를 사용했다는 점이 문제가 되므로 객체지향 재귀를 사용하지 않았다.

알려진 스몰토크 사용예

Collection

Collection은 스몰토크에서 사용되는 Composite 패턴의 가장 일반적인 예제이면서 유용성이 가장 적은 경우이기도 하다. 이것은 Component 클래스 (Object), Composite 클래스 (Collection), 수 많은 Leaf 클래스들(그 외 Object의 모든 서브클래스)로 패턴을 구현한다. Collection의 한 요소는 또 다른 Collection이 될 수 있으므로 구조는 무제한 중첩을 지원한다.

Object는 Collection이 Composite 오퍼레이션으로서 구현하는 오퍼레이션을 정의한다. 다시 말해, Object는 수신자의 클래스를 표시하기 위해 printString을 정의한다. Collection은 Collection의 클래스뿐만 아니라 Collection 내 요소들의 printString들도 나타내기 위해 printString을 재구현한다. 하지만 Object는 맞춤형 인터페이스를 갖고 있지 않으므로 Collection도 마찬가지일 것이다. 이는 Composite 패턴의 도메인 특정적 예제와는 거리가 멀다.

Visuals

스몰토크 주요 방언들은 윈도윙 시스템의 구현에 Composite 패턴을 사용한다. 비주얼은 재귀적으로 중첩이 가능하므로 어느 정도 패턴의 꽤 엄격한 예제라고 볼 수 있겠다.

필드 서식 명세

파일 리더(Woolf, 1997)는 한 번에 각 레코드의 필드 하나씩을 읽음으로써 레코드위주의 플랫 파일을 읽어낸다. 필드그룹이 단일 필드처럼 행동하는 기능이 바로 Composite 패턴의 예가 된다. 파일 리더에 사용되는 Composite 패턴은 3개의 클래스, FieldFormatDescription (Component) 클래스와 두 개의 서브클래스인 LeafFieldFormat (Leaf), CompositeFieldFormate (Composite)를 이용해 구현된다. Composite들은 무제한 중첩을 지원한다.

미디어 요소

Composite 패턴은 EFX라는 디지털 영상 편집 및 특수 효과 환경에서 (Alpert et al., 1995) 집합 (aggregate) 및 원시 (primitive) 미디어 요소를 일관적으로 나타내기 위해 사용되었다. EFX의 사용자 인터페이스는 영상 합성의 내용을 명시하기 위해 수평적 시간선을 통합한다. 사용자는 미디어 요소, 즉 비디오 클립, 오디오 세그먼트, 특수 효과등을 나타내는 직사각형 모양의 막대를 추가한다. 시간 선을 따라 막대의 위치와 너비는 재생 시간과 구간을 결정한다. 편집기는 시간에 맞춰 다수의 미디어를 동시에 발생이 가능하도록 다수의 트랙을 제공한다.

언제라도 원하는 트랙에 있는 다수의 미디어 요소를 선택하여 그룹화시킬 수 있다. 그 결과 하나의 그룹 요소가 되는데, 시각적으로는 하나의 트랙에 그룹화된 미디어 요소로서 표시된다. 이러한 집합 요소들은 개별 미디어 요소들과 동일한 핵심 프로토콜을 이해한다. 모두가 시간선에 어떻게 자신을 그리는지 알고, 모두 기간을 증가 또는 감소시키기 위해 트리밍 메서드를 제공하며, 모두 시작시간과 지속 기간에 대한 접근자 메시지를 구현한다. 그룹 요소는 중첩 또한 지원하기 때문에 원시 요소와 그룹 요소 모두 더 큰 그룹 요소로 합해질 수 있다. 이러한 중첩 기능은 Composite의 엄격한 정의를 지원한다.

문장 구조

자연언어 파서인 (Alpert & Rosson, 1992) ParCE는 객체로서의 문장 성분을 나타낸다. 이것은 하나의 문장을 파스(parse) 트리로 분석한다. 트리에서 비단말 문장성분(nonterminal constituent) 객체(Composite)들은 문장, 명사구, 전치사구 등을 나타낸다. Composite는 궁극적으로는 적절한 단말 성분 객체를 포함한다 (Leaf는 명사, 동사, 한정사 등을 표시). 이 모든 성문 객체들은ㅡ각 단어, 구, 문장ㅡ파싱, 인쇄, 접근에 대해 동일한 프로토콜에 응답한다. 문장이 길수록 깊이 중첩된 트리, 패턴의 엄격한 정의로 나타난다.

위탁 계정

재정 도메인을 모형화하는 프레임워크는 주로 Composite 클래스를 Account와 같은 것으로 구현하여 위탁 계정을 표현한다. Asset나 Security와 같은 클래스는 Component와 Leaf 클래스들을 정의한다. Account는 다른 Asset들로 구성된 Composite 자산을 정의한다. 그 외 Composite 자산은 고객의 Portfolio, 브로커의 BrokerAccountsUnderManagement, 브랜치의 BranchAccountsUnderManagement가 될 수 있다. (자세한 내용은 예제 코드 단락을 참조) Asset 서브클래스는 각각 다른 Asset 구체적 서브클래스의 자산만 포함할 수 있기 때문에 중첩은 구체적 계층의 수로 한정되어 있으며, 이에 따라 Asset은 Composite 패턴의 광범위한 해석으로 볼 수 있다.

SingleCollection

SignalCollection은 비주얼웍스의 Collection 계층구조에 위치한 Composite 클래스이다. 이것은 Signal들의 집합체를 포함하는 것이 아니라 Signal들의 집합체이다.

비주얼웍스는 두 개의 구분된 클래스, Signal과 Exception로서 예외 처리를 구현한다. Signal은 발생할 수 있는 오류 유형을 설명하고; Exception은 구체적 오류를 설명한다. Exception은 어떠한 오류가 발생했는지 알기 위해 그것의 Signal을 참조한다. 이것은 Type Object 패턴의 예제이다 (Johnson & Woolf, 1988).

Exception을 잡아두려면(trap) 개발자는 잡아둘 Exception을 가진 Signal을 명시해야 한다. Signal은 계층구조를 이루기 때문에 Signal는 스스로 또는 자식 Signal을 위해 어떤 Exception도 잡아둘 것이다. 하지만 그 계층구조의 다른 부분으로부터 다수의 Signal을 잡아두려면 프로그래머가 각 Signal을 개별적으로 명시하고 전체 목록에게 Exception을 잡아두라고 말해야 한다. 프로그래머는 Signal 루트를 포함한 SignalCollection을 생성시킨 후 단일 Signal인 것처럼 Exception을 잡아둘 것을 알림으로써 이를 해결할 수 있다.

Signal과 SignalCollection은 Exception을 가두는 동일한 인터페이스를 가지고 SignalCollection은 그 행위를 Signal들에게 위임함으로써 실행되기 때문에 이 두 클래스는 Composite 패턴의 예로 볼 수 있다. Composite 클래스가 Collection 계층도에 구현되는 다른 예제에서와 마찬가지로 이번 예제 또한 Signal과 SignalCollection이 동일한 계층도에 구현된다면 더 낫겠다. SignalCollection이 다른 SignalCollection들을 포함하는 경우는 드물기 때문에 중첩되지 않을 것이므로 이 예제는 패턴의 광범위한 해석으로 볼 수 있겠다.

Composite ProgramNodes

SequenceNode는 비주얼웍스의 ProgramNode 계층구조에 있는 Composite 클래스이다. 스몰토크는 소스 코드를 ProgramNode인 파스 노드의 트리로 컴파일한다. LiteralNode, BlockNode, AssignmentNode, ConditionalNode와 같이 다양한 프로그래밍 구성마다 다른 ProgramNode 서브클래스가 있다. SequenceNode는 메서드 또는 블록 내의 일련의 문을 (예: 코드의 라인) 나타낸다. 각 문(statement)은 그 자체가 ProgramNode로서 Composite 패턴의 예가 된다.

ProgramNode 계층구조에서 Composite 패턴의 좀 더 미묘한 예로 ConditionalNode를 들 수 있다. ConditionalNode에는 3가지 주요 부분이 있다: condition, trueBody, falseBody가 그것이다. 이들은 다음과 같은 ifTrue:ifFalse: 문을 형성한다.

condition ifTrue: trueBody ifFalse: falseBody

이들 각 부분은 자체가 ProgramNode이다. ConditionalNode는 한정된 자식 수를 가진 Composite 패턴의 예제이다ㅡ이번 경우에는 3가지 주요 부분에 대한 자식 변수 3개가 된다.

ProgramNode 계층구조에서 또 다른 Composite 패턴의 예로 ArthmetricLoopNode, AssignmentNode, CascadeNode, LoopNode, SimpleMessageNode, MessageNode를 들 수 있다. 이 클래스들은 모두 다수의 ProgramNode를 포함하고 있는데 이를 통합하여 단일 ProgramNode의 역할을 한다. 파스 노드 트리는 수 많은 중첩 가능성을 가지므로 매우 깊게 퍼질 수 있으므로 Composite 패턴의 꽤 엄밀한 해석에 해당하겠다.

CompositeFont

CompositeFont는 비주얼웍스의 ImplementationFont 계층구조에 있는 Composite 클래스이다. 이 계층도는 다음과 같은 형태를 띤다:

Object ()
	ImplementationFont ()
		CompositeFont (currentFont fonts ...)
			DeviceFont ()
				...
			SyntheticFont (baseFont ...)

ImplementationFont는 폰트를 표준 객체 인터페이스로 조정하는 Adapter (105) 패턴이다. DeviceFont는 실제로 플랫폼에 있는 자체 폰트 중 하나를 조정한다. SyntheticFont는 Decorator (161) 패턴으로서 플랫폼의 폰트에서 이용할 수 없는 효과를 추가한다. CompositeFont는 다수의 플랫폼 폰트를 결합하여, 다국어 문자처럼 단일 플랫폼 폰트에서 이용할 수 없는 문자들을 형성한다. 이로 인해 결합된 폰트가 하나의 폰트로서 역할을 한다.

위의 예제는 Composite와 Decorator에 모두 해당된다. Component 클래스 (ImplementationFont), Leaf / ConcreteComponent (DeviceFont), Composite (CompositeFont), Decorator (SyntheticFont)를 갖는다. 하지만 CompositeFont들은 중첩되는 경우가 거의 없으므로 Composite 패턴의 광범위한 해석을 나타낸다.

FontDescriptionBundle과 SPSortedLines

FontDescriptionBundle은 비주얼웍스의 Composite 클래스이다. 그것의 Leaf 클래스는 FontDescription이다. 비주얼웍스는 해석에 일치하는 최근 플랫폼에서 자체 폰트를 검색할 때 FontDescription을 사용한다. FontDescription은 사실상 폰트 범위를 설명하는 FontDescriptionBundle이 될 수도 있다.

패턴에서 권하듯 FontDescription과 FontDescriptionBundle은 동일한 계층구조에 구현되지 않았기 때문에 Composite 패턴으로 인정하기가 힘들다. 대신 FontDescription은 객체의 서브클래스이고 FontDescriptionBundle은 Collection 계층구조에 있다.

SPSortedLines는 비주얼웍스의 Composite 클래스이고 그 Leaf 클래스는 SPFillLine으로서, 다른 라인 세그먼트와 상호작용하는지 결정하기 위해 특수 행위를 가진 라인 세그먼트이다. SPSortedLines는 그러한 라인 세그먼트의 집합체이다. 이는 Composite와 Leaf 클래스가 동일한 계층구조에서 구현되는 Composite 패턴의 또 다른 예가 되겠다.

FontDescriptionBundle와 SPSortedLines는 주로 중첩되지 않으므로 패턴의 약한 예가 되겠다.

합성수

Composite Numbers

일부 Number 클래스는 실제로 다른 Number들을 이용해 구현된다. 이들이 바로 합성수이다. 플랫폼 데이터 타입이 아닌 모든 Number 클래스는 (예: 정수, 실수 또는 상수) 아마도 합성수일 것이다. 이러한 합성수는 Composite 패턴의 예가 된다. 즉, 스몰토크의 Fraction 클래스는 합성수이다. 이는 두 개의 수를 포함하고 있으며ㅡ분자와 분모ㅡ이를 이용해 값을 계산한다. 두 수를 나눌 때 Float 대신 Fraction을 결과로 저장하면 반올림 오차를 피할 수 있다 (효율성은 해치겠지만). Fraction은 단일 Number의 역할을 하지만 두 개의 구분된 Number로 구성되므로 Composite 패턴이라 할 수 있다.

또 다른 예로, 비주얼웍스의 FixedPoint와 IBM 스몰토크의 Decimal과 같은 고정 소수(fixed decimal)을 들 수 있다. 고정 소수는 반올림 오차를 피하기 위해 분수 부분이 정수로 저장된다는 점만 제외하면 변동 소수점과 같다. 이는 단순한 수, 주로 Integer로부터 구성되기 때문에 복합 합성수가 된다.

합성수는 주로 중첩되지 않는데, 그 이유는 중첩된 수 트리를 단일 합성수로 감소시킬 수 있기 때문이다. 예를 들자면, 분자와 분수도 Fraction인 Fraction을 굳이 저장할 이유가 없다; Integer 컴포넌트를 이용해 정확도를 떨어뜨리지 않고 하나의 분수로 단순화시킬 수 있다. 하지만 합성수를 공격적으로 사용하면 단순화시킬 수 없는 트리로 이끌어, 이러한 중첩된 트리는 패턴의 엄밀한 해석을 따르게 된다.

Beck (1996)는 좀 더 도메인 특정적인 합성수를 나타내는 Impostor라는 패턴을 간략하게 논한 바 있다. 그는 MoneySum이라는 객체를 구현하는 패턴을 이용하는데, 이 패턴은 하나의 통화로 환전하지 않고 두 개의 통화(currency)를 이용해 두 개의 Money 객체를 추가한다. 실제로 Money들을 추가하는 대신 MoneySum이 Money들을 비롯해 후에 Money로 추가된 다른 것들을 함께 저장한다. 후에 특정 통화로 값을 표시해야 하는 상황과 같이 총액이 실제로 필요한 상황이 오면 MoneySum이 한 번에 모든 환전을 수행한다; 내용을 해당 통화로 환전하여 추가한다. 한 번에 모든 환전을 수행하는 것은 수 많은 중간 단계를 거치는 것보다 더 효율적이다. MoneySum과 Money는 동일한 인터페이스를 가지기 때문에 서로 대체하여 사용이 가능하다. 따라서 MoneySum은 Composite Money가 된다. Impostor는 다른 Impostor를 포함하며, 이는 패턴의 엄밀한 해석을 뒷받침한다.

관련 패턴

Commposite, Decorator, 그리고 객체지향 재귀

Composite와 Decorator (161)는 주로 동일한 계층구조에서 함께 사용되는데, 이는 두 패턴 모두 Component의 인터페이스를 클라이언트가 구조 내 어떠한 노드로든 사용할 수 있는 핵심 인터페이스로 제한할 것을 요구하기 때문이다. 어떤 타입의 인터페이스가 한 패턴에 맞게 제한되고 나면 나머지 패턴은 그 핵심 인터페이스를 지원하도록 쉽게 적용이 가능하다. 광범위한 Component 인터페이스를 단순화된 핵심 인터페이스로 압축시키는 것은 까다로운 일이지만, 어쨌든 성공하고 나면 Composite 또는 Decorator 중 하나를 조정하기란 쉽다.

Composite는 객체지향 재귀를 통해 그 Component들과 의사소통을 한다 (Chain of Responsibility (225) 참조). Leaf로 끝이 나는 중첩된 일련의 Composite들은 연결고리를 형성한다. 이러한 연결고리에서 상위 Composite가 메시지를 수신하면 이 Composite는 메시지를 자식에게 전달한다. 메시지가 Leaf까지 다다르고 나면 최종적으로 처리된다.

Chain of Responsibility는 이 3가지 패턴을 함께 사용하는 것과 관련해 더 많은 정보를 제공한다.

Composite 패턴 대 Decorator 패턴

트리는 주로 Leaf 노드뿐만 아니라 Composite 노드와 Decorator (161) 노드 둘 다 포함한다. 이 둘을 구분하기 위해서는 하나의 노드에 얼마나 많은 컴포넌트가 있는지를 살펴본다. Leaf 노드에는 확실히 컴포넌트가 없으며, Decorator 노드는 정확히 하나의 컴포넌트, Composite 노드에는 다수의 컴포넌트가 있다. 노드가 다수의 컴포넌트를 가질 수 있으나 하나의 컴포넌트 또는 아예 없는 경우라 하더라도 다수의 컴포넌트를 가질 수 있기 때문에 Composite 노드가 된다.

Iterator

Composite 패턴은 때때로 Iterator (273) 패턴을 사용해 자식에게 메시지를 전달한다.

Notes