DesignPatternSmalltalkCompanion:ChainsofResponsibility
CHAIN OF RESPONSIBILITY (DP 223)
의도
요청을 처리할 수 있는 기회를 하나 이상의 객체에게 부여함으로써 요청하는 객체와 처리하는 객체 사이의 결합도를 없앤다. 요청을 해결할 객체를 만날 때까지 객체 고리를 따라서 요청을 전달한다.
구조
일반적 객체 구조와 메시지 상호작용은 다음과 같은 형태를 띨 것이다:
논의
Chain of Responsibility 패턴에서는 다음 객체로의 참조에 의해 연결된 객체들의 고리를 따라 어느 한 객체가 요청을 처리하겠다고 결정할 때까지 요청이 전달된다. 요청을 보낸 클라이언트는 최종적으로 어떤 객체 또는 처리기(handler)가 요청을 처리하게 될지 알지 못한다. 이로 인해 클라이언트에 영향을 미치지 않고 고리를 재정렬하여 이를 따라 책임성을 재분배시킬 수 있다.
고리는 단순하게 연결된 리스트지만 트리의 일부가 되는 것이 보통이다. (트리 구조는 Composite (137) 패턴을 참조.) 객체 고리는 클라이언트 처리기부터 트리의 루트까지 경로를 말한다. 아래 다이어그램은 일반적인 트리를 나타냄과 동시 클라이언트가 그것의 처리기를 가리키는 경우이다; 클라이언트의 처리기로부터 루트까지의 책임연쇄(Chain of Responsibility) 경로를 굵은 글씨로 표시하였다:
책임연쇄 트리연쇄반응트리는 단순한 트리 구조가 아니라 단계마다 가장 전반적 수준부터 가장 구체적 수준까지 특이성 수준을 나타내는 계층으로 구성된다:
클라이언트는 처리기로 요청을 보내면서 가능한 한 구체적인 응답을 요청한다. 따라서 클라이언트의 처리기는 매우 특수화되어 있어야 한다. 처리기가 요청을 처리할 수 없을 경우 자신보다 조금 더 포괄적인 처리기로 전달하여 요청을 처리하며, 응답을 리턴할 수 있는 처리기가 나타날 때까지 계속 전달된다. 트리는 루트에 있는 가장 전반적 처리기부터 잎(leaf)에 있는 가장 구체적 처리기까지 구성되어 있으며, 요청의 처리는 그 클라이언트에게 가장 구체적인 처리가 될 것이다.
군대 생각하기
Chain of Responsibility 패턴의 가장 훌륭한 예는 사실 객체지향 프로그래밍과 전혀 상관이 없다; 바로 군대의 명령계통이다. 행동을 하기 전에 허락을 받아야 하는 이등병을 생각해보면 그는 병장에게 권한을 요청한다. 병장 또한 권한이 많지 않으므로 요청을 또 상관에게 전달하고, 이런 과정이 반복하다 결국 누군가 결정을 내린다. 그리고 나면 그 결정은 다시 내려와 결국 이등병에게 전달된다. 이등병은 어떤 단계에서 이러한 결정을 내렸는지 알지 못한다; 병장일 수도 있고 합동참모본부까지 올라갔을지도 모른다. 단순한 요청은 명령계통에서 높이 올라가지 않아도 되지만 그렇지 않을 시 복잡해져 높은 계통까지 올라간다.
명령계통은 정적이거나 불변의 구조가 아니다. 퇴각, 진급, 새로운 곳으로 발령되는 사람들이 생기면서 이러한 계통에 참가자와 그들의 배열은 변한다. 특정 결정을 내리는 단계는 당시 그 위치에 있는 사람들의 권한에 따라, 그리고 부대가 전투, 훈련연습, 포상 휴가 중인지에 따라 바뀐다. 따라서 이러한 고리는 변화하는 조건에 따라 역동적으로 응답하고 그에 따라 결정을 내리는 권한을 재분배할 수 있다.
이것이 Chain of Responsibility 패턴의 간결한 예이다. 누군가 Chain of Responsibility라고 말한다면 군대의 명령계통을 생각하라. 그러면 패턴의 좋은 예제가 생각날 것이다.
클래스 계층구조
스몰토크의 클래스 계층구조는 Chain of Responsibility 패턴이 어떻게 작용하는지를 잘 보여주는 좋은 예지만 패턴을 어떻게 구현하는지는 보여주지 않는다. 클래스의 루트인 Object는 가장 전반적인 클래스이고, 그에 따른 서브클래스들은 점점 구체적이 된다. 각 클래스는 그의 슈퍼클래스를 알고 있으며, 슈퍼클래스가 스스로 구현하지 않을 행위를 상속받는다.
객체가 메시지를 수신하였는데 이에 대한 응답으로 실행할 메서드를 찾아야 한다고 가정해보자. (이는 메서드 룩업이 어떻게 논리적으로 행동하는지를 설명해준다.) 당신에겐 4개의 클래스가 있고ㅡA, B, C, DㅡD는 C의 서브클래스이고, C는 B의, B는 A의 서브클래스이다. B는 operation을 구현하고, C와 D는 그 구현을 변경하지 않은 채 상속한다고 치자 (다음 페이지 참조):
클라이언트가 operation 메시지를 D의 인스턴스 aD로 전송하면 B의 메서드를 어떻게 호출할까? 수신자 aD는 그의 클래스인 D에게 메시지 operation의 구현을 요청한다. D에는 operation 메서드가 없으므로 이 클래스는 그의 슈퍼클래스인 C에게 메서드의 구현을 요청한다. C도 operation의 구현 메서드를 갖고 있지 않으므로 그의 슈퍼클래스에게 요청한다. 마침내 operation의 구현을 찾은 B 클래스가 메서드를 리턴한다. C는 B로부터 받은 메서드를 D로 전달하여 최종적으로 메서드를 호출하는 aD로 전달된다. 이번 예제에선 operation의 구현부 요청이 A까지 전달되지 않기 때문에 A가 operation을 구현 가능한지 여부는 상관이 없다.
위의 그림은 Chain of Responsibility가 어떻게 작용하는지를 보여준다. 클라이언트 aD가 operation의 구현을 요청한다. 클라이언트는 그의 처리기 D로 요청을 전송한다. 요청을 스스로 처리할 수 없는 이 처리기는 다음 객체인 C로 요청을 전달하고, C는 다시 B로 전달한다. B 처리기는 요청을 처리할 수 있으므로 그렇게 한다. 요청 경로가 풀리면서 응답은 B에서 C, D, 그리고 요청을 한 클라이언트 aD까지 차례로 전달되어 실제로 누가 요청을 처리했는지도 모른 채 응답을 받는다.
클래스 계층구조에 있는 메서드 룩업이 Chain of Responsibility가 어떻게 작용하는지를 보여주긴 하지만, 패턴의 고리가 클래스가 아닌 인스턴스의 고리여야 하기 때문에 패턴 자체로는 부족한 예제가 되겠다. 패턴의 두 번째 결과는, 고리가 런타임 시 역동적으로 변할 수 있다는 점이다 (DP 226). 클래스와 그가 구현하는 메서드는 런타임 시 정적인 경향이 있다; 컴파일 없이는 변하지 않는다. 진정한 Chain of Responsibility패턴을 나타내는 트리는 클래스 계층구조와 같이 컴파일 시간에 하드코딩된 것이 아니라 런타임 시 구축된 구조이다.
Signal 트리
그렇다면 트리 단계에 따라 특이성 수준이 달라지는 객체 트리의 좋은 예제로 어떤 것이 있을까? 비주얼웍스의 예외처리 메커니즘에서 찾을 수 있다. 발생 가능한 각 오류의 유형은ㅡ이해되지 않은 메시지 또는 발견되지 않은 키ㅡ오류를 설명하는 Signal의 인스턴스로 표시된다.
Signal 인스턴스는 루트 Signal genericSignal 가 모든 신호를 나타내는 계층구조를 형성한다. 주 자식인 Object errorSignal은 모든 런타임 코드 오류 신호를 나타낸다. 이의 자식들은 이보다 좀 더 구체적인 오류 유형을 설명해가고 프로그래머에게 무엇이 정확히 잘못됐는지 말해줄 수 있을 만큼 구체적인 설명이 나올 때까지 내려간다. 아래 다이어그램은 표준 신호 트리의 일부인, 발견되지 않은 오류에 대한 가지(branch)를 보여준다:
대부분의 일반 객체들이 가장 상위에 존재하고 가장 구체적 객체들이 바닥에 위치하므로 괜찮은 Chain of Responsibility 트리의 예제이다. 이는 상속에 대한 객체 버전의 종류로서 (클래스 버전에 반해) 각 자식은 부모로부터 속성을 상속 받지만 특수화시키진 않는다.
예외처리 프레임워크이 이 패턴에 대한 완벽한 트리를 나타내긴 하지만 실제로 Chain of Responsibility 패턴을 사용하진 않는다. Singal에서 Chain of Responsibility에 가장 가까운 예제는 accepts:에 대한 구현이다. 신호가 특정 신호 계층구조에 속하는지 검사한다는 점에서 신호 트리의 isKindOf: 로 볼 수 있다. 예를 들면,
Object notFoundSignal accepts: Object nonIntegerIndexSignal
not-found는 non-integer-index의 조상이므로 위는 사실이다. 이와 비슷하게, Object errorSignal 는 코딩 실수에 관한 모든 오류 신호를 수신하지만 이를 제외한 신호는 (예: 사용자 인터럽트 신호) 수신하지 않는다.
accept: 메서드는 다음과 같이 구현된다:
Signal>>accepts: aSignal
^aSignal inheritsFrom: self
Signal>>inheritsFrom: aSignal
^aSignal == self or:
[parent notNil and: [parent inheritsFrom: aSignal]]
수신자가 그 신호이거나 그 조상들 중 하나가 그 신호일 경우 수신자는 aSignal을 수락한다. 따라서 accepts: 가 실행될 때마다 신호들 중 하나가 true를 리턴하거나 최종적으로 루트 신호가 false를 리턴한다. 경로에 있는 다른 신호들은 aSignal을 처리할 것인지를 결정하고 부모에게 책임을 전달한다. 기본적으로 Chain of Responsibility패턴의 예제에 속하긴 하나 그다지 흥미롭지는 않다.
Chain of Responsibility 패턴의 예제로서 신호 트리를 이용할 때 또 다른 약점은 트리가 런타임 시 그 구조를 변경한다거나 책임을 재분배하지 않는다는 점이다. 상상컨대 시스템은 실행하는 동안 새로운 유형의 오류를 감지해야 할 필요를 발견할 수 있으며, 그럴 경우 시스템은 신호 트리로 오류 유형을 추가할 것이다. 예를 들어, 특정 운영체제만이 기호 연결 파일을 지원한다. 비주얼웍스가 이러한 운영체제에서 실행되고 있다는 사실을 감지하면 무효한 기호연결을 발견하기 위한 신호를 로드할지도 모른다; 그리고 다른 운영체제로 전환 시에는 이 신호를 언로드할 것이다. 비주얼웍스는 이런 방식으로 작동이 가능하지만 실제로 실행하진 않는다. 시스템이 실행 중인 경우 신호 트리의 구조는 정적이다.
흔치 않은 예
스몰토크에서 Chain of Responsibility 패턴이 사용되는 예는 거의 없다. 특수화에 따라 구성된 트리 또는 연결 리스트를 필요로 하는데, 시스템이 실행 시 역동적으로 재구성되고 책임을 재분배할 수 있을 뿐 아니라, 각 행위가 객체고리의 한 처리기에서 구현되는 동안 나머지 객체들은 단순히 부모로 책임을 전달하는 조건을 모두 만족해야 한다.
객체지향 재귀
Chain of Responsibility 패턴은 객체지향 (OO) 재귀라 부를 좀 더 광범위한 패턴의 특수화된 부분이라 예제를 찾긴 힘들지만 여전히 배워두면 유용하다. Chain of Responsibility패턴과 객체지향 재귀 패턴은 서로 다르지만 그 원칙은 꽤 비슷하여 하나를 이해하고 나면 나머지를 이해하기가 훨씬 쉽다. 게다가 Chain of Responsibility 패턴의 예제는 드물지만 OO 재귀 패턴의 예제는 넘쳐난다.
Chain of Responsibility 패턴에서 처리기는 다음 둘 중 하나만 수행할 수 있다: (1) 요청을 완전히 처리하여 요청에 대한 고리의 응답을 리턴하거나, (2) 요청 처리를 돕기 위한 일은 전혀 하지 않고 후임자(successor)에게 요청만 전달한다. 즉 고리에 포함된 처리기들 중 요청을 처리하는 처리기는 하나임을 의미한다; 클라이언트와 실제 처리기 사이에 위치한 다른 처리 객체들은 결국 요청을 전달하고 응답을 전달하는 일 외에는 하지 않을 것이다.
처리기가 요청을 전달하는 동안 약간의 추가 작업을 할 때가 있는데 이것이 바로 OO 재귀 패턴, 또는 재귀적 위임이라 부르기도 한다. 절차적 재귀에서 함수는 매번 다른 파라미터로 스스로를 호출한다. 최종적으로 파라미터는 함수가 실행되는 기본적 경우로, 재귀가 본래 호(call)로 돌아간다 (Sedgewick, 1988, 제 5장). 객체지향 재귀 패턴에서 메서드는 그 메시지를 서로 다른 수신자에게 다형적으로 전송한다 (동일한 클래스의 또 다른 인스턴스 또는 같은 유형의 다른 클래스의 인스턴스가 될 수도 있다). 결국 호출된 메서드는 업무를 실행하는 기본적 경우의 구현이며 이후 재귀는 본래 메시지 전송으로 되돌린다 (Beck, 1997).
Chain of Responsibility와 OO 재귀 패턴 간 또 다른 중요한 차이점은, 후자의 경우 구조의 특수화 단계와 관련이 적다는 점이다. OO 재귀 패턴은 구조 내 노드들이 왜 그렇게 배열되어 있는지에 관한 것이 아니라 요청을 달성하기 위해 요청을 재귀적으로 가로지를 수 있다는 것과 관련되어 있다. 따라서 트리는 특수화, 포함(containment), 실행순서(ordering), 또는 다른 관계에 따라 배열되었을지도 모른다. 또한 재귀는 트리의 상위방향을 이동할 뿐 아니라 하향으로도 이동이 가능하여, 가지 노드에 있는 다수의 작은 재귀로 나뉜다. 재귀는 구조에서 포인터가 향한 방향은 어디든 이동할 수 있다.
트리 재귀
OO 재귀 (Chain of Responsibility) 행위는 연결 리스트 구조를 따라 발생한다; 여기서 리스트는 고리를 의미한다. 단 스몰토크에서 연결 리스트를 찾기가 힘들다. 하지만 트리 구조는 매우 흔하며, 루트와 잎 노드 사이의 경로는ㅡ어떤 방향이든ㅡ기본적으로 연결 리스트이기 때문에 OO 재귀는 요청을 전달하거나 트리의 상향과 하향 방향으로 내용을 알리는데 주로 사용된다. 그래프 구조에서 각 경로 또한 연결 리스트이지만 그래프는 트리보단 덜 사용된다. 트리 또는 그래프에서 노드들 사이의 포인터는 한방향 또는 양방향이 가능하다. (트리 구조에 관한 논의는 Composite (137)를 참조한다.) 고리는 포인터에 의해 형성되므로 재귀는 포인터의 방향으로만 이동할 수 있다.
연결 리스트 또는 트리는 요소들을 모으는 하나의 방법에 불과한 듯 보일지 모르지만 이러한 연결 구조들은 대부분 Collection들이 구현되는 방식과는 꽤 다르다. 일반적인 Collection은 2층 구조로 되어 하나의 객체, Collection 자체가 구조 내에 어떤 요소들이 있는지를 알고 있다. 따라서 얼마나 많은 요소들이 있는지, 각 항목이 무엇인지 등을 쉽게 목록으로 만들 수 있다. 예를 들어, OrderedCollection과 그 요소들은 다음과 같이 연관된다:
그래프나 트리의 경로 또는 연결 리스트는 다계층 구조로서 어떤 노드도 다른 노드에 대해 알지 못한다. 대신 각 노드는 그 다음 노드가 무엇인지 알기 때문에 결국 순회할 수 있다. 하지만 이는 얼마나 많은 요소들이 있는지 또는 각 요소가 무엇인지 결정하는 것과 같은 흔한 업무를 다계층의 과정으로 만든다. 다음은 연결 리스트의 노드들이 어떻게 서로 연관되는지를 보여준다:
연결 리스트의 요소들은 OO 재귀가 통해가는 고리를 형성한다. Collection의 요소들은 고리를 형성하지 않으므로 OO 재귀는 요소들을 순회할 수 없다.
비주얼 트리
비주얼웍스에서 윈도우 창을 구현하는데 사용되는 비주얼 트리는 몇 가지 OO 재귀의 사용예를 포함한다. 비주얼 트리 구조에 관한 상세한 내용은 Composite (137)를 참조한다.
비주얼은 그 경계를 계산할 때 (VisualComponent>>bounds) OO 재귀를 사용한다. 대부분 비주얼은 주어진 공간을 채우기에 필요한 만큼 크게 스스로를 그린다. 따라서 그들이 우선하는 경계는 무제한이다. 하지만 비주얼의 실제 경계는 컨테이너 비주얼의 경계에 의해 제한되는데 컨테이너 비주얼은 또 다시 컨테이너의 제약을 따른다. 최종적으로 윈도우 창 프레임이 모든 구성요소의 경계를 제약한다.
컨테이너의 관계로 인해 비주얼은 그 경계를 모른다. 이 경계는 경계가 어디 포함되는지와 컨테이너의 경계가 무엇인지에 따라 달라진다. 아래 다이어그램은 클라이언트가 비주얼에게 그 경계를 요청하였을 때 일어나는 일을 설명하는 예이다:
메시지의 구현을 다이어그램에서 살펴보자. 비주얼로 bounds를 전송하면 VisualPark에 구현부를 호출한다:
VisualPart>>bounds
"Answer the receiver's compositionBounds if there
is a container, otherwise answer preferredBounds."
^container == nil
ifTrue: [self preferredBounds]
ifFalse: [container compositionBoundsFor: self]
인스턴스 다이어그램은 VisualPart의 컨테이너가 nil이 아니므로 bounds가 컨테이너로 compositionBoundsFor: 를 전송하는 것을 보여준다. 컨테이너는 Wrapper로서, Wrapper는 VisualPart로부터 compositionBoundsFor: 를 상속받는다:
VisualPart>>compositionBoundsFor: aVisualPart
"The receiver is a container for aVisualPart.
An actual bounding rectangle is being searched for by
aVisualPart. Forward to the receiver's container."
^container compositionBoundsFor: self
이러한 구현부는 컨테이너 관계를 통해 비주얼 트리의 위로 이동하는 compositionBoundsFor: 의 재귀를 나타낸다. 결국 재귀는 종료 case, 즉 윈도우 창 자체와 같은 바운딩 비주얼에서 끝이 난다. 바운딩 비주얼은 경계를 계산할 뿐이다:
ScheduledWindow>>compositionBoundsFor: aVisualComponent
"Answer the receiver's bounds."
^self bounds
이는 윈도우 창에 대한 경계와 재귀 풀기(recursion unwind)를 리턴한다.
간단한 예를 살펴본 결과 어떤, 비주얼의 경계든 그 윈도우 창의 경계와 동일하다고 암시할지도 모르지만 그것은 사실이 아니다. 많은 비주얼 클래스, 특히 Wrapper (Decorator (161) 패턴)는 그 컴포넌트의 경계에 영향을 미친다. 예를 들어, TranslatingWrapper는 그 좌표를 해석함으로써 자신의 컨테이너로부터 비주얼의 오프셋을 조정한다. 이는 compositionBoundsFor: 의 구현의 특수화를 통해 이루어진다:
TranslatingWrapper>>compositionBoundsFor: aVisualPart
^(container compositionBoundsFor: self)
translatedBy: self translation negated
윈도우 창 그래픽의 무효화와 재표시하기 또한 OO 재귀를 사용한다. 클라이언트가 비주얼에게 invalidate 를 명령하면 VisualPart>>invalidateRectangle:repairNow:forComponent: 가 정의한 메시지가 윈도우 창이 처리할 때까지 위로 올라간다. 클라이언트가 윈도우 창에게 ScheduledWindow>>displayDamageEvent: 를 통해 재표시하도록 명령할 경우 VisualComponent>>displayOn: 에 의해 정의된 메시지는 각 비주얼이 스스로를 다시 표시하는 동안 트리의 깊이 우선으로 하향 재귀한다.
마지막으로, 비주얼웍스에서 비주얼 트리의 어떤 Controller가 제어를 원하는지 결정할 때도 도 OO 재귀 패턴을 사용한다. 폴링 (polling) 윈도우 창에서 ScheduledWindow>>subViewWantingControl은 VisualPart>>objectWantingControl과 CompositePart>>componentWantingControl이 번갈아 트리를 타고 내려가는 재귀를 시작한다. 이벤트 구동의 창에서는 VisualPart>>handlerForMouseEvent: 는 비주얼 트리를 재귀적 하향하는 동안 컨트롤러는 마우스 이벤트의 수신자를 검색한다.
필드 설명 트리
파일 리더(Woolf, 1997)는 레코드의 양식이 설명된 필드 양식 객체의 트리를 이용하는 레코드 지향 파일이다. 프레임워크는 FormattedStream 이라는 특별한 클래스를 포함하고 있으며, 이 클래스는 파일을 읽어 도메인 객체로 레코드를 매핑하기 위해 필드 양식 트리와 일반 스트림을 결합한다. (FormattedStream 에 관한 자세한 정보는 Decorator (161)를 참조한다.)
Stream에 속하는 FormattedStream은 next를 구현하여 파일로부터 다음 레코드를 읽어온다. FormattedStream>>next는 FieldFormatDescription>>readObjectFrom:into: 에 의해 정의된 재귀 메시지를 시작한다. 메시지는 필드 양식 트리를 재귀 하향하고 도메인 객체로 레코드를 읽어온다. next가 어떤 필드 양식이 요청을 처리할지 모른 채 읽기 요청을 시작하는 것이 OO 재귀의 예제가 된다. 필드 양식 트리에 있는 노드는 고리를 형성한다. 그리고 각 노드는 레코드의 일부를 읽어내는 책임을 수행한다.
FormattedStream은 도메인 객체를 레코드로서 파일로 작성하기 위해 nextPut: 을 구현하기도 한다. nextPut: 은 재귀 메시지 FieldFormatDescription>>writeObjectIn:to:를 시작한다. 메시지는 필드 양식 트리를 재귀 하향하고 도메인 객체를 레코드에 작성한다. nextPut:의 이러한 재귀적 구현은 OO 재귀의 또 다른 예에 해당한다. 필드 양식 트리 노드들은 트리를 구성하며, 각 노드는 객체를 레코드로 작성하는 책임을 수행한다.
필드 양식 트리가 어떻게 작용하는지에 관한 상세한 내용은 예제 코드 단락을 참조한다.
Interpreter트리
Interpreter은 표현식 트리를 가로질러 트리가 나타내는 정규 표현식을 평가한다 (DP 244). 트리를 가로지르기 위해 클라이언트는 인터프리터 메시지를 트리의 루트 노드로 전송한다. 노드는 그 자식들을 차례차례 해석함으로써 자신을 해석한다. 해석 메시지가 트리를 통해 재귀하는 것 또한 OO 재귀의 한 예가 된다.
클래스 계층구조에서 OO 재귀
약간 덜 분명한 OO 재귀의 예제는 계층구조에서 클래스들을 타고 올라가면서 구현되는 메시지들이다. 다시 말하지만, 클래스 트리는 정적인 트리라서 런타임 시 변경되지 않지만, 객체 고리로 이루어진 집합으로서 트리에 속하므로 OO 재귀를 구현할 수 있다.
initialize의 구현은 클래스 계층구조에 사용되는 OO 재귀의 훌륭한 예제이다. 클래스 D는 C의 서브클래스이고, C는 B의, B는 A의 서브클래스로서, 이 4개의 클래스는 initialize를 구현한다. A를 제외한 클래스의 각 구현부는 super initialize를 전송할 것이다:
A class>>new
^self basicNew initialize
A>>initialize
...
^self
B>>initialize
super initialize.
...
^self
C>>initialize
super initialize.
...
^self
D>>initialize
super initialize.
...
^self
클라이언트가 D의 인스턴스를 생성하여 new가 initialize를 실행하면 D의 initialize 메서드가 C의 메서드를 실행할 것이고, B와 A에 위치한 메서드가 차례로 실행될 것이다. 이러한 방식으로 각 클래스는 객체에 대한 고유의 부분을 초기화를 책임진다. 이는 클래스 계층구조 내에서 정적으로 코딩되는 OO 재귀이다.
Equal, Hash, 그리고 Copy
스몰토크에서 흔히 사용되는 다수의 메시지들은 주로 OO 재귀를 사용하여 구현되는 것이 보통이다. equal (=), hash, 그리고 copy 메시지들이 3가지 예가 된다.
Object에서 이퀄의 기본적 구현은 그다지 흥미롭지 않다. 기본적으로 이퀄은 더블 이퀄로서 (==) 구현된다. 하지만 도메인 객체에 대한 클래스와 같이 구조적으로 복잡한 클래스들은 훨씬 더 흥미로운 방식으로 이퀄을 구현하곤 한다. 전형적인 Person 객체가 이퀄을 어떻게 구현하는지 살펴보자. 예를 들어, name이 동일하면 두 개의 Person 객체가 동일하다고 치자:
Object subclass: #Person
instanceVariableNames: 'name address phoneNumber'
classVariableNames: ''
poolDictionaries:''
Person>>= aPerson
"Two Persons are equal if their names are equal."
(aPerson isKindOf: Person) ifFalse: [^false].
^self name = aPerson name
Object subclass: #PersonName
instanceVariableNames: 'firstName lastName'
classVariableNames: ''
poolDictionaries:''
PersonName>>= aPersonName
"Two PersonNames are equal if their
first names and last names are equal."
(aPersonName isKindOf: PersonName) ifFalse: [^false].
^self firstName = aPerson firstName)
and: [self lastName = aPerson lastName]
String>>= aString
"Two Strings are equal if their characters are equal."
"This example is from VisualWorks."
| size |
aString isString ifFalse: [^false].
aString isSymbol ifTrue: [^false].
(size := self size) = aString size ifFalse: [^false].
1 to: size do:
[:index | (self at: index) = (aString at: index)
^true
이 코드에서 나타내는 바와 같이 두 개의 PersonName이 동일하면 두 개의 Person 객체는 동일하며, PersonName이 동일하려면 성과 이름 String 모두가 동일해야 하며, 이는 다시 Character들이 동일해야 한다는 조건이 붙는다. Person에서 PersonName으로, 다시 String과 Character로 위임되는 이퀄이 OO 재귀이다. 각 객체는 자신의 계산 부분을 책임진 후 나머지 책임을 적절한 속성에 위임한다.
이퀄을 구현하는 어떤 클래스든 hash 또한 구현해야 한다 (Beck, 1997; Woolf, 1996). 클래스의 hash 구현부는 이퀄이 위임되는 속성과 같은 속성에게 위임해야 한다. 즉, 우리의 Person 클래스는 name 속성으로 이퀄을 위임하므로 hash 또한 name 속성으로 위임해야 한다:
Person>>hash
"A Person's hash value is the same as its
name's hash value."
^self name hash
PersonName은 동등(equality)에 대한 두 가지 속성을 사용하며, 두 가지 모두 해시(hash) 및 결합되어야 한다:
PersonName>>hash
"A Person's hash value is a combination of its
parts' hash value."
^self firstName hash bitXor: self lastName hash
따라서 Person또한 이퀄과 마찬가지로 OO 재귀를 이용해 hash를 구현하므로, 이를 PersonNam으로 위임하고 이는 다시 성과 이름으로 위임된다.
객체가 스스로 깊은 복제를 (예: 완전히 종속적인) 생성할 수 있는 기본 알고리즘은 다음과 같다: 객체가 자신과 그의 모든 부분들을 복사하고, 그 부분들이 다시 자신과 부분들을 복사해 나가는 과정이 반복되는 것이다. 이 또한 OO 재귀이다. copy의 자세한 내용은 Prototype (77)을 참조한다.
객체지향 트리
객체지향 프로그래밍에서 배울 수 있는 가장 흔하면서 가장 강력한 기법들 중 하나는 객체지향 트리를 구성하는 방법이다. 이러한 트리들은 3가지 디자인 패턴을 통합한다: Composite (137), Decorator (161), OO 재귀 패턴이 그것이다. Composite 패턴은 구조 패턴으로서 다형적 노드들로부터 트리를 생성한다. Decorator 패턴은 구조를 재정의하여 가지 또는 잎 노드에 새로운 행위의 추가를 가능하게 만든다. 그리고 OO 재귀 패턴은 요청에 행위를 제공하여 트리가 트리의 구조를 알 필요 없이 함수를 실행하도록 해준다.
대부분 Composite 구조들은 Decorator 패턴도 포함한다. Composite 패턴은 Component의 구현을 두 개의 서브클래스, Composite와 Leaf로 나누기 때문에 서브클래싱을 통해 Component로 행위를 추가하기가 힘들다. 서브클래스는 Composite나 Leaf에 영향을 미치지 않기 때문에 Component의 서브클래싱은 작용하지 않을 것이다. 행위를 추가하기 위해 Composite 또는 Leaf 를 상속할 경우 당신은 다른 상속과 코드의 중복을 필요로 할 것이다. Decorator 패턴은 서브클래싱에 대한 좀 더 유연한 대안책으로서 구조 내의 노드에 추가 행위를 동적으로 추가할 수 있도록 해준다. 따라서 Component의 Decorator 서브클래스에서 추가 행위를 구현함으로써 Composite 또는 Leaf에 행위를 Decorator로서 추가할 수 있다.
Composite와 Decorator를 함께 사용하는 것은 편리할 뿐 아니라, 두 패턴의 디자인은 서로 잘 어울린다. 본 저서의 Composite와 Decorator 패턴 설명 부분을 통해 component 슈퍼클래스가 서브클래스의 핵심 인터페이스를 선언하는 것을 논한 바 있다. 이 논의 부분에서는 서브클래스에서의 핵심 인터페이스의 확장이 클라이언트가 기대하는 다형성을 망칠 수 있으니 주의해야 한다고 언급한다. 이로 인해 Composite 또는 Decorator 클래스에 해당하는 클래스들은 꽤 제한된 인터페이스를 가졌음에도 불구하고 전체 기능성 집합을 제공한다. 첫 패턴을 (Composite 또는 Decorator) 적용하기 위해 Component의 인터페이스를 제한되게 사용하기란 까다롭지만 우선 성공하고 나면 두 번째 패턴은 훨씬 더 수월하게 구현할 수 있다. 주로 Composite 패턴이 먼저 적용되어 트리 구조를 정의한다. 이후 Decorator를 적용하는 것이 상대적으로 쉬운데, 이는 Composite가 이미 Component의 제한된 핵심 인터페이스를 정의해놨기 때문이다.
트리가 함수를 실행하도록 요청하기란 힘들다. 트리는 하나의 객체가 아니라 계층적으로 배열된 객체의 집합체이기 때문이다. 따라서 트리가 전체로서 함수를 실행하기 위해선 그의 모든 노드에게 함수를 실행하라고 말해주어야 한다. 함수를 트리의 모든 노드에서 실행시키는 절차는 트리의 전체적 구조에 대해서는 많은 추측을 할 수 없는데, 이것은 구조가 쉽게 변할 수 없기 때문이다. 따라서 절차는 트리 구조 스스로 각 노드에 함수를 실행하도록 맡겨두어야 한다. 이렇게 트리 구조로 미루는 절차는 OO 재귀 패턴의 예가 된다.
이에 따라 Composite, Decorator, 그리고 OO 재귀 패턴은 흔히 함께 사용된다. Composite 패턴은 객체지향 트리를 정의한다. 이것이 완료되면 Decorator와 OO 재귀 패턴은 트리의 기능성과 유연성을 향상시킨다.
활용성
Chain of Responsibility와 OO 재귀 패턴을 적용 시 두 가지 제약이 있다:
- 고리가 이미 존재한다. 고리가 없는 경우 고리를 생성하지 않는다. [디자인 패턴] 편에서는 그렇지 않다고 말하지만 (DP 226) 행위 패턴이 순회하게 될 링크를 생성하는 구조 패턴을 적용해야 고리가 생긴다는 표현이 더 맞겠다. 이러한 고리는 트리 또는 그래프에 있는 연결 리스트나 경로가 된다. 고리에서 각 노드는 마지막에 위치한 것보다 더 포괄적일 경우 Chain of Responsibility에 고리를 사용할 수 있다. 그렇지 않을 경우 좀 더 일반적인 패턴인 OO 재귀를 사용한다.
- 집합체는 고리가 아니다. Collection은 (예: OrderedCollection 또는 Array) 이 패턴에 관해서는 고리가 아니다. 다이어그램에서 나타내듯 고리에서 각 노드는 후임자를 가리킨다. Collection에서 요소들은 서로를 알지 못하므로 어떤 요소도 다음 요소가 무엇인지 모른다. Chain of Responsibility와 OO 재귀는 Collection 구조를 가로지를 수 없다; 대신 클라이언트는 Iterator (273)을 사용해야만 한다.
구현
Chain of Responsibility와 OO 재귀 패턴을 구현할 때 고려해야 할 몇 가지 문제가 있다:
- 스몰토크에서의 자동 전달. [디자인 패턴]에 설명된 이 기법은 단순해 보이지만 피해야 한다. 처리기는 각 메시지를 개별적으로 그 후임자에게 전달하는 대신 자신이 이해하지 못하는 메시지를 doesNotUnderstand: 의 구현을 통해 모두 후임자로 전달할 수 있다. 이 기법은 매우 유연하지만 상당히 무계획적이다. 이러한 구현 기법과 그 위험 요소에 관한 자세한 내용은 Proxy (213) 패턴을 참조한다.
- 일을 처리하거나 책임을 전달하거나, 혹은 일을 처리한 후 책임을 전달하거나. [디자인 패턴] 편에서는 객체 고리의 각 처리기는 요청을 완전히 처리하여 연쇄적 처리를 중단하거나, 아무 일도 하지 않은 채 요청을 후임자로 전달하는 일 중 하나를 택할 수 있다. 이것은 Chain of Responsibility패턴이다. 스몰토크에서는 흔히 3번째 옵션을 선택하는데, 이는 바로 처리기가 요청을 부분적으로 처리하고 나머지는 후임자로 전달하는 방식이다. 이러한 요청 처리 방식은 처리기들을 통해 퍼진다. 이것은 OO 재귀 패턴에 해당한다.
- 재귀 방법의 배치. OO 재귀에서 재귀는 3가지 방법으로 구성된다: 이니시에이터, 리커서, 터미네이터가 있다. 터미네이터 메서드는 리커서 메서드의 기본 case이므로 다형적이다. 이니시에이터 메서드의 경우, 재귀를 시작하는 메시지와 재귀적 메시지 자체는 달라야 하기 때문에 리커서나 터미네이터에 대해 다형적일 수 없다. 다음 페이지의 표에서 보이는 바와 같이 이러한 메시지의 구현 장소는 재귀가 이동하는 방향에 따라 달라진다:
Method's Class | ||||
Direction | Initiator | Recurser | Terminator | |
Up the tree | Leaf, Component, or Client | Component | Component | |
Down then tree | Component or Client | Composite | Leaf |
- 트리의 상향 재귀. 트리에서 잎 노드에서 루트 노드까지 경로는 연결 리스트이다. 이니시에이터 노드는 잎이 되므로 이니시에이터 메서드는 Leaf 노드에서 구현된다. 어떤 노드로부터 재귀가 시작될 경우 이니시에이터 메서드는 Component 클래스에서 구현된다. 이니시에이터 메서드는 Clinent 클래스에서도 구현이 가능하다. 재귀는 어떠한 노드를 통하든 이동이 가능하므로 리커서 메서드는 Component 클래스에 구현된다. 어떤 노드든 루트의 역할을 할 수 있으므로 터미네이터 메서드 또한 Component 클래스에 구현된다.
- 트리의 하향 재귀. 루트 노드부터 잎 노드까지 트리를 하향으로 재귀하는 것은 전체 트리를 주로 깊이 우선으로 순회한다. 따라서 재귀가 Composite 노드에 닿으면 가지로 나뉜다; 하나의 자식을 위한 경로를 재귀하고 푼 후에 다음 자식을 위한 경로를 재귀하고 풀며, 나머지 자식에게도 반복해간다. 재귀가 어떠한 노드부터든 시작할 수 있을 경우 이니에이터 메서드는 Component에서 구현된다. 이는 Client 클래스에서도 구현이 가능하다. 리커서 메서드는 Composite 클래스에서 구현된다; 그것의 각 가지에서 재귀를 실행함으로써 계속 이어갈 수 있다. 마지막으로, 터미네이터 메서드는 Leaf 클래스에서 구현된다; 일반적으로 터미네이터 메서드는 Component 클래스로 상향 이동할 이유가 없다.
예제 코드
앞서 논한 바와 같이 파일 리더는 (Woolf, 1997) OO 재귀의 몇 가지 예를 구현한다. 트리를 생성하여 도메인 객체의 구조로 레코드 필드의 매핑을 설명하며, 특별한 스트림을 이용해 레코드에서 도메인 객체로서 읽어온다.
Stream에 속하는 FormattedStream은 파일로부터 다음 레코드를 읽어오기 위해 next를 구현한다. next는 필드 양식 트리를 하향 재귀하는 재귀 메시지 readObjectFrom:into:를 초기화하여 레코드를 도메인 객체로 읽어온다:
Stream subclass: #FormattedStream
instanceVariableNames: 'dataStream streamFormat ...'
classVariableNames: ''
poolDictionaries: ''
FormattedStream>>next
"See superimplementor."
...
^streamFormat readObjectFrom: dataStream
Object subclass: #StreamFormatDescription
instanceVariableNames: 'dataFormat resultChannel ...'
classVariableNames: ''
poolDictionaries:''
StreamFormatDescription>>readObjectFrom: dataStream
...
dataFieldFormat
readObjectFrom: dataStream
into: resultChannel.
^self result
Object subclass: #FieldFormatDescription
instanceVariableNames: ''
classVariableNaems: '...'
poolDictionaries: ''
FieldFormatDescription>>readObjectFrom: dataStream into:
aValueModel
"Reads the field that the receiver describes from
dataStream and stores the field's value in aValueModel."
self subclassResponsibility
필드 양식의 유형마다 (서브클래스) 데이터 스트림으로부터 데이터를 읽어내는 절차가 서로 다르다. 가장 단순한 경우, 잎 필드 양식은 다음 필드로부터 데이터를 읽고 변환한 후 반환할 객체에 저장한다.
FieldFormatDescription subclass: #LeafFieldFormat
instanceVariableNames: 'readSelector writeSelector'
classVariableNames: ''
poolDictionaries: ''
LeafFieldFormat>>readObjectFrom: dataStream into: aValueModel
"See superimplementor."
| bytes fieldValue |
...
bytes := self readFieldFrom: dataStream.
...
fieldValue := bytes perform: readSelector.
aValueModel value: fieldValue
복합 필드 양식은 그의 자식 필드를 레코드로부터 재귀적으로 읽어냄으로써 레코드로부터 자신을 읽는다:
FieldFormatDescription subclass: #CompositeFieldFormat
instanceVariableNames: 'fieldFormats resultChannel
fieldAdaptors'
classVariableNames: ''
poolDictionaries: ''
CompositeFieldFormat>>readObjectFrom: dataStream into:
aValueModel
"See superimplementor."
...
self readFieldsFrom: dataStream
CompositeFieldFormat>>readFieldsFrom: dataStream
"Read the fields out of dataStream into the result object."
1 to: self numberOfFields
do:
[:i |
| field adaptor |
field := self fieldAt: i.
adaptor := fieldAdaptors at: i.
field readObjectFrom: dataStream into: adaptor]
데코레이터 필드 양식은 읽기 요청을 그 컴포넌트로 단순히 전달할 뿐이다:
FieldFormatDescription subclass: #FieldFormatDecorator
instanceVariableNames: 'fieldFormat'
classVariableNames: ''
poolDictionaries: ''
FieldFormatDecorator>>readObjectFrom: dataStream into:
aValueModel
"See superimplementor."
fieldFormat readObjectFrom: dataStream into: aValueModel
FormattedStream은 writeObjectIn:to: 라는 재귀적 메시지를 사용하여 레코드로서 도메인 객체를 쓰기 위해 nextPut: 또한 구현한다.
FormattedStream>>nextPut: anObject
"See superimplementor."
streamFormat writeObject: anObject to: dataStream
StreamFormatDescription>>writeObject: anObject to: dataStream
...
resultChannel value: anObject.
dataFieldFormat writeObjectIn: resultChannel to: dataStream
FieldFormatDescription>>writeObjectIn: aValueModel to:
dataStream
"Writes the value in aValueModel to sourceStream
using the field format described by the receiver."
self subclassResponsibility
writeObjectIn:to:의 재귀는 readObjectFrom:into:와 마찬가지로 구현된다. 이는 트리의 하향으로 재귀하므로 키 구현자는 CompositeFieldFormat과 LeafFieldFormat이 된다. FieldFormatDecorator는 메시지를 전달할 뿐이다.
Composite 패턴의 예제 코드 단락에서도 OO 재귀 패턴을 설명하고 있다.
알려진 스몰토크 사용예
Chain of Responsibility 패턴이 스몰토크에서 사용되는 경우는 드물다. 대부분 트리는 특수화가 아닌 구성(composition)에 따라 조직된다. 그리고 대부분 처리기는 일을 처리와 위임 중 하나를 선택하는 것이 아니라 일을 처리하고 위임한다.
OO 재귀는 스몰토크에서 매우 흔히 사용된다. 물론 연결 리스트 구조는 스몰토크에서 거의 사용되지 않지만 트리 구조는 흔하다. 사실상 스몰토크의 모든 트리 구조는 행위를 분배하는데 있어 OO 재귀를 사용한다.
관련 패턴
OO 재귀 패턴과 Composite, Decorator, Adapter 패턴들
Composite (137) 패턴이 메시지를 자식에게 위임하거나 Decorator (161)가 그 컴포넌트로 메시지를 위임할 때 OO 재귀 패턴이 사용된다. 물론 고리가 다형적이진 않지만 이와 비슷하게 어댑티로 위임하는 Adapter (105) 또한 OO 재귀 패턴이다. 하나의 객체가 협력자에게 위임하는 것은 그다지 확장적인 고리가 아니므로 이 모든 예제들은 변질(degenerator)에 가깝다.
OO 재귀를 보여주는 더 괜찮은 예제는 일련의 Composite, Decorator, Adapter 들이 고리를 형성할 때이다. 그리고 난 후 메시지는 고리의 처음부터 시작해 자신을 처리할 처리기를 찾아 고리를 따라 이동한다. 클라이언트는 어떤 노드가 요청을 처리하게 될지 모른다. 고리를 형성하는 것은 구조 패턴이지만 모드를 따라 순회하는 메시지의 행위는 OO 재귀 패턴이다.
OO 재귀 패턴 대 Iterator 패턴
Iterator (273) 패턴은 구조 내의 각 노드를 고려하여 다른 노드들로부터 떨어져 각자 노드에서 함수가 실행될 수 있다. Iterator는 어떤 노드가 요청을 처리할지를 안다; 최근 반복 처리된 노드이다. Iterator는 Iterator는 각 노드를 차례로 고려하도록 보장한다.
OO 재귀 패턴은 구조 내 각 노드를 별개로 고려하지 않는다. 대신 클라이언트가 노드 중 하나로 요청을 하면 해당 노드는 다른 노드로 요청을 전달하여 누군가 처리할 때까지 전달된다. 구조에 속한 모든 노드들은 이러한 과정에 포함될 수 있고, 아니면 이에 참여하는 첫 번째 노드가 되기도 한다. 어디에 해당하든 클라이언트는 최종적으로 어떤 노드가 참여하는지 알지 못한다.
OO 재귀 패턴과 Iterator 패턴
이 두 패턴은 물론 다른 패턴이긴 하지만 Iterator 패턴의 예제는 OO 재귀 패턴을 사용해서 구현할 수 있다. 트리로 구현된 Collection의 서브클래스가 do: 를 어떻게 구현할 것인지 생각해보라. 루트 노드는 do: 블록을 각 노드에게 차례로 재귀적으로 전달하여 노드가 스스로 블록을 평가할 수 있게 된다. 각 composite 노드는 자체적으로 블록을 평가하여 그의 자식에게 요청을 재귀적으로 전달할 것이다. 각 잎 노드는 단순히 자체적으로 블록을 평가하는 데에 그칠 것이다. 이런 방식으로 트리를 통한 절차의 반복은 항상 모든 노드를 순회하는 루트로부터 재귀로 구현될 것이다.
OO 재귀 패턴과 Interpreter 패턴
Interpreter 패턴은 추상 구문 트리를 순회하기 위해 해석 메시지를 이용한다. 이러한 순회는 OO 재귀 패턴의 예제에 속한다.