SmalltalkObjectsandDesign:Chapter 18
- 제 18 장 디자인 패턴
디자인 패턴
이제 객체와 그것을 이용해 스몰토크에서 프로그래밍하는 것과 관련해 어느 정도 살펴보았으니 더 높은 추상화 수준-디자인 패턴-을 따져볼 위치에 있다. 패턴은 모든 활동에서 발생한다; 소프트웨어라고 특별한 것은 아니다. 훌륭한 체스 선수는 말의 움직임에 가능한 모든 조합을 다 생각하지 않는다; 수학적으로 불가능하다[1]. 대신 자신이 살펴본 조합의 수를 제한하기 위해 위치 또는 패턴에 대해 마음 속 저장소를 그려본다. 대상이 체스든 소프트웨어든 패턴에 대한 개인의 저장소가 바로 초보자와 전문가를 구별하는 요소다.
체스 선수의 패턴이 그가 플레이한 위치, 그리고 다른 게임에서 학습한 위치에서 비롯되듯이 디자이너의 패턴 역시 스스로 알아낸 것과 다른 사람의 디자인에서 목격한 것으로 구성된다. 이번 장에서는 다른 사람들의 디자인에서 증명된 패턴과 함께 디자인 패턴에 대한 본인만의 저장소를 준비시켜 도움을 주는 것을 목적으로 한다.
객체 지향 디자인 패턴은 훌륭한 디자인에서 계속해서 발생하는 클래스 또는 객체의 그룹화로 생각하면 된다[2]. 이러한 그룹화는 자연스럽게 더 큰 규모의 재사용으로 향하는 신호다; 초기에는 객체 공동체들이 훌륭한 객체 또는 클래스를 만드는 것이 무엇인지에 초점을 두었는데, 이제는 훌륭한 객체 또는 클래스의 그룹화를 만드는 것으로 전향되고 있다. 우리가 소개할 첫 번째 예제는 (215 페이지) 본 저서에서 이미 3번이나 발생한 패턴으로, Account와 그 트랜잭션 로그(transaction logs), AddressBook과 그 내부의 연락처(contacts), ShapeRoom과 그 도형(shapes)이 되겠다. 이 패턴은 똑똑한 컨테이너에 해당하는데, 가령 주소록이 연락처를 포함하지만 연락처의 휴대전화 등으로 전화를 걸 때 회사 또는 우편 번호로 검색할 수 있을 만큼 똑똑하기 때문이다.
이번 장에 실린 패턴들은 필자도 즐겨 사용한다. 모든 디자이너들은 자신이 선호하는 패턴이 있기 마련인데, 이 패턴들 대부분은 까다로운 문제를 해결하도록 도와준 것들이다. Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides는 23개의 패턴 목록을 구성했는데 이제는 표준 참고 도서가 되었다 [Gamma et al. 1995]. 당신은 디자인 시 직면하는 문제에 적용되는 패턴들을 기반으로 자신만의 색다른 목록을 발전시킬 수 있을 것이다.
이것들은 디자인 패턴들이므로 이번 장에 실린 스몰토크 코드를 전부다 쓸 수 있을 것이라고 기대하진 말길 바란다. 패턴은 재사용 가능한 디자인이지 재사용 가능한 코드는 아니다. 설명은 어떻게 각 패턴이 문제를 해결하도록 돕는지 상상하기에 충분한 세부 내용을 포함하지만, 자신의 문제에 패턴을 완전히 적용하기 위해서는 여전히 자신의 에너지를 어느 정도 투자해야 할 것이다. 그래야 여기서 다룬 기본 기법들의 변형(variations)도 알아낼 수 있을 것이다.
표기법
패턴의 핵심을 전달하도록 돕기 위해선 어느 정도 표기법이 필요하다. 우리가 사용할 표기법은 대부분 OMT를 기반으로 하며, 예를 들자면 다음과 같다 [Rumbaugh et al. 1991].
3개 부분으로 된 직사각형 상자는 클래스다; 맨 위는 클래스명, 맨 아래는 적절한 메서드를 열거하고, 중간은 적절한 인스턴스 변수를 포함한다. 삼각형은 상속을 의미하고, 다이아몬드는 집합(aggregation)을 의미한다. 색칠된 원은 위의 orchestra 내 다수의 instruments와 같이 한 클래스의 다수의 인스턴스를 수반할 수 있는 관계 또는 연관(association)을 나타낸다. 메서드의 로직을 설명해야 하는 경우 Violin>>play처럼 모서리가 둥근 상자를 사용할 것이다. 그리고 subclassResponsibility 메서드는 제 5장부터 그래왔듯이 별표로 표기할 것이다. 이러한 표기법은 풍부한(full-bodied) 디자인 방법론의 표기법과는 거리가 멀지만 설명을 시작하기엔 충분하다.
제 6장에서 집합과 컨테이너 사이에 모호한 영역에 대해 언급한 바가 있다. 그런데 집합과 일대다 또는 일대일 연관 사이에도 비슷한 영역이 존재한다. 집합(aggregation)은 특히 강력한 유형의 연관(association)으로, 디자이너는 "part-of(~의 일부)"라고 말하는 것이 적절한 설명이라고 생각한다. 하지만 어떤 디자이너는 위에 설명한 orchestra와 instruments 간 일대다 관계와 같은 것을 연관이라고 부르지만 또 다른 디자이너는 집합이라고 부를지도 모른다. 후자에 해당하는 디자이너는 instruments가 orchestra의 일부 또는 orchestra가 instruments를 갖고 있음을 나타내는 데에 다이아몬드 표기법을 사용해왔을 것이다.
두 명의 디자이너는 또한 orchestra와 conductor 간 사이에 대해서도 옥신각신할 수 있다. 한 명은 일반적인 일대일 연관이라 주장하고, 나머지 하나는 위에서 보이는 바와 같이 좀 더 특별한 집합이라고 주장할 수 있다. 이는 판단의 문제다. 선택은 자신이 해결하고 있는 문제의 영향을 받기 마련이다 (여기서는 아직 문제를 제시하지 않았기 때문에 전혀 도움이 되지 않는다.)
예를 들어, conductor의 스케줄과 업무를 시뮬레이트하는 데 발생한 문제는 관계를 완전히 역전시키고 conductor가 orchestra를 갖도록 선언하게끔 보장할지도 모른다.
이러한 미묘한 차이는 디자인이란 정밀하지 않은 기술임을 상기시켜주지만, 이번 장에선 별 문제가 되지 않는다. 우리는 표기법을 개념을 표현하고 모호함을 피하기 위한 용도로만 사용할 것이기 때문이다.
똑똑한 컨테이너 (컬렉션-작업자로 알려짐)
Checking account, address book, shape editor에 공통된 디자인을 살펴보면서 익숙한 영역부터 시작해보자. 이들은 아래와 같은 모습을 할 것이다:
Account 클래스는 transactions를 "포함"하기 때문에 개념적으로 컨테이너와 같으면서도 트랜잭션을 처리하고 잔고를 조정한다는 의미에서 똑똑하다고 말할 수 있다. 실제 컨테이너는 스몰토크에서 내장된 컨테이너인데, 이번 경우 SortedCollection의 인스턴스가 되겠다. 이와 비슷하게 AddressBook 클래스는 기본적으로 person 객체를 포함하지만 스몰토크에 내장된 컬렉션 클래스가 가진 행위 이상의 행위를 (contacts 관리용) 가진다. 그리고 ShapeRoom 클래스는 개념적으로 shapes를 포함하지만 이를 이리저리 이동시키고 심지어 사용자의 액션을 undo할 수도 있다.
세 가지 예제에서 모두 적절한 항목에 대한 추가, 제거, 또는 중지(holding on)의 작업은 똑똑한 컨테이너가 아니라 내장된 스몰토크 컬렉션 클래스에게 주어진다. 똑똑한 컨테이너는 그 항목을 포함하는 것으로 보일 뿐이다. Peter Coad는 이를 "기본 패턴" 또는 컬렉션-작업자(collection-worker) 패턴이라 부른다; 이는 본 저서에 실린 대부분의 패턴에서 기반이 된다 [Coad et al. 1995].
구체화(reification)
구체화는 객체가 아닌 것으로 보이는 무언가를 객체로 바꿔 놓음을 의미하는 광범위한 용어다. 이것을 디자인 패턴이라고 부르기가 공평하지 않을 정도로 광범위하다. 오히려 다른 디자인 패턴들이 발생하는 메타 패턴이라고 보는 편이 옳겠다.
가장 눈에 띄는 구체화의 예는 디자이너가 메서드 또는 동사와 같은(verb-like) 아이디어를 객체로 취급할 때 발생한다. 176 페이지의 undo 연습에서 명령(commands)과 액션이 객체의 클래스가 되는 것을 목격했을 것이다. 이 구체화에 대해서는 다음 절에서 다시 살펴보겠다.
사실 활동이나 행위는 항상 메서드로서 시작하지만 시간이 지나면서 복잡해진 변형(variant)이 되고 그 활동은 클래스에 훌륭한 후보(candidate)가 되며 변형은 서브클래스의 후보라는 느낌이 들곤 한다. 이것이 바로 undo 상황에서 발생한 일이다. 특정 유형의 액션-이동, 생성, 제거-이 서브클래스가 되었다. 우리는 각 연산(operation)을 고유의 클래스에서 구체화하였다.
또 다른 공통된 일례로 검색(searching)을 들 수 있다. 언뜻 보기에 객체의 저장소를 검색하는 것은 메서드의 평범한 일로 보인다. 하지만 머지않아 검색을 요청하는 행위가 풍부한 활동이 될 수 있음을 깨닫는다; 검색을 명시하는 데에 있어 온갖 방법이 존재할 수 있다. 이러한 발견을 하고 나면 재빠르게 구체화로 이어진다: 검색에 대한 기준을 설명하는 다양한 arguments를 나타내는 인스턴스 변수를 이용해 Search라는 클래스를 정의한다. search 객체는 약간의 다른 기준으로 다시 검색을 원할 때 즉시 유용해진다. 머지않아 당신은 다른 저장소에서 다른 종류의 객체를 검색하고 싶어져 각 객체 유형마다 구분된 Search 서브클래스를 정의하고 있을 것이다. 검색에 사용되는 무해한 동사 같은(verb-like) 행위는 여러 잠재적 클래스로 성장해왔다.
우선 무언가를 클래스로 구체화하고 나면 주요 public 메서드는 종종 실제 루틴명을 가진 것으로 나타나는데, Command 클래스에 대한 undo를 예로 들 수 있겠다. Search 예제에서 이름은 execute 또는 doIt이 될 가능성이 크다. 그 다음 메서드는 각 구체적 서브클래스에서 다형적으로 재구현되므로, 모든 종류의 Commands는 스스로를 어떻게 undo하는지 알며 모든 종류의 Search는 스스로를 어떻게 execute(실행하는지) 안다. 이러한 관찰은 한 가지 경험에 의한 규칙을 설명한다: 구체화는 다형성을 야기한다. 메서드로부터 클래스로 구체화할 때마다 다형성의 혜택을 즐길 것이다.
가장 극적인 구체화는 메서드로부터 시작되지만 모두 그런 것은 아니다. 한 가지 일반적인 형태는 객체의 두 클래스 간 관계로부터 시작된다: Student와 Course 클래스 간 다대다 관게의 intricacies를 관리하기 위해 StudentCourse 클래스를 도입할 경우 구체화로서 count as한다. 예를 들어, grades(성적)와 attendance(출석률)는 Student(학생)이나 Course(수강과목)의 인스턴스보다는 StudentCourse의 인스턴스에서 더 잘 캡슐화된다. 이러한 구체화 형태는 연관 클래스로 알려져 있는데, grades 또는 attendance와 같은 정보는 링크 속성으로 알려진다 [Rumbaugh et al. 1991].
구체화는 메서드보다 큰 규모의 활동에서 시작되기도 한다. 예를 들어, [Jacobson et al. 1991]은 전체 use case가 객체에 적절한 후보자가 될 수 있다고 제시한다. 활동을 구체화할 때는 전체 use case 또는 좀 더 일반적인 활동이 되도록 하여 결과가 되는 객체를 control 객체라고 부르도록 하라. 다시 말해, control 객체는 활동의 구체화인 셈이다. (이러한 control 객체를 MVC의 컨트롤러 객체와 혼동하지 않도록 하라.)
일반적으로 구체화는 평범한 시작에서 객체 지향 훌륭함으로 디자인이 발전됨을 보인다. Draw 메서드는 마침내 DrawingTool 클래스가 된다; 가령 MM/DD/YY 문자열 포맷을 date 객체로 번역하기 위한 일반적 메서드는 Convert 클래스(VisualAge에서는 AbtConverter)가 된다; 비트맵을 복사하는 연산은 결국 BitBlockTransfer 클래스가 된다(VisualWorks에서는 BitBlt). 구체화에서 얻을 수 있는 중요한 교훈은, 디자인에서 문제가 복잡해지기 시작할 경우 숙련된 디자이너라면 한 걸음 뒤로 물러나 완전히 새로운 객체 유형을 도입하는 가능성을 고려한다. 거의 구체화 자체가 객체 지향 디자인이라고 말할 수도 있겠다. 이러한 정신으로 커맨드 패턴을 다시 학습하길 바란다.
커맨드(command)
앞에서 undo 문제를 해결한 바가 있을 것이다; 이제는 패턴-커맨드 패턴-의 형태로 만들어보자.
❏ 액션이나 명령의 undo를 위해서 이 디자인을 어떻게 완료할 것인가?
해답: 앞에 실린 논의를 (177페이지부터 시작) 다른 이름을 이용해 살펴보겠다. Command>>execute와 Command>unExecute는 Command 클래스 내의 subclassResponsibility(순수 가상) 메서드들이며, Command의 각 서브클래스에 구체적 실현(concrete realization)을 갖고 있다. 공통 객체의 연표(chronology)를 유지하기 위해선 Stack 클래스가 필요하다. Stack의 LIFO(last-in-first-out) 규칙은 가장 최근 명령이 가장 먼저 undo되도록 보장한다. 결과는 다음과 같다:
위 그림은 커맨드 패턴을 설명할 뿐만 아니라 애초에 Command 클래스를 만들어내는 것이 구체화의 전형적 예가 된다는 사실을 인지해야 한다. Undo 해결책에서 발견한 중요한 내용은, undo 문제에서 move, remove 또는 command 와 같이 동사와 같은(verb-like) 아이디어가 객체란 사실을 받아들이는 것이었다.
팩토리 메서드
하나의 클래스가 특정 기능을 위해 다른 클래스에 결정적으로 의존하는 경우가 발생하곤 한다. 예를 들어, 클라이언트/서버 시스템에서 데이터베이스 서버와 스몰토크 클라이언트 간에 고객 정보를 상호간 전달하는 클래스는(일반적으로 CustomerBroker 클래스라 부른다) 새 customer 객체를 생성할 때마다 Customer 클래스를 필요로 한다. 사무실 데스크톱 애플리케이션의 경우라면, Calculator 클래스가 화면에 스스로를 표시할 때 표준 CalculatorWindow 클래스가 필요한 것과 마찬가지다.
애플리케이션의 라이프 사이클 초기에는 디자이너가 다른 클래스 쌍들도 같은 관계를 갖고 있음을 인정한다: OrderBroker가 새 order 객체를 생성할 때 Order를 필요로 하거나, Phonebook이 스스로를 표시할 때 PhoneWindow를 필요로 하는 상황. 이러한 상황은 아래와 같은 코드를 야기하곤 한다:
작지만 눈에 거슬리는 중복이 보일 것이다: 메시지 ...new openWidget가 정확히 같은 형태로 두 번 나타난다.
❏ 이러한 중복은 간단하지만 감각적인 개선을 필요로 한다. 서브클래스로부터 공통된 ...new openWidget 코드를 팩토링하여 슈퍼클래스 DesktopObject로 올려야 한다. 하지만 이유가 뭘까?
해답: 이치에 맞는 유일한 방식으로 DesktopObject>>openWindow에 대한 코드를 작성하라: self windowClass new openWidget. 여기서 windowClass 메서드는 서브클래스를 따라야 한다. 즉, 각 서브클래스는 그것과 연관된 클래스를 리턴하는 windowClass를 지원해야 한다. 결과는 다음과 같다:
이 패턴은 팩토리 메서드라고도 알려지는데, 객체가 관련된 클래스의 인스턴스를 생성할 때 자주 발생하기 때문이다. 패턴의 최소 표현주의(minimalism)을 주목하라: 각 클래스와 각 메서드는 그것이 해야 하는 일만 수행할 뿐, 그 이상은 수행하지 않는다.
이러한 패턴은 추상 클래스를 수반하는 디자인이라면 그 질을 나타내는 훌륭한 지표가 된다. 이 패턴은 디자이너가 클래스들 간에 명확한 관계를 설계하고자 노력했을 때에만 디자인에 발생한다. 반대로 어떤 곳에서도 이 패턴을 볼 수 없다면 클래스와 그들의 간계가 되는 대로 배치되어 있음을 나타낸다.
레코드로부터의 객체
레코드로부터의 객체 패턴은 이번장의 다른 패턴들에 비해 좀 더 특수화되고 복잡하다. 이는 서버측의 일반적인 플랫, 관계형 데이터베이스와 클라이언트 워크스테이션측의 객체 간을 왔다갔다하면서 오늘날 가장 근본적인 클라이언트/서버 문제를 해결한다.
프로그래머가 항상 이해해왔듯이, 레코드는 단순히 바이트의 문자열로서, 이는 다시 구분된 필드로 다시 나뉜다. 역사적으로 레코드는 파일 또는 "데이터세트"에서 끝과 끝을 이어주는 데이터의 조각이었다. 오늘날 우리는 일반적으로 관계형 데이터베이스 테이블에서 행의 형태로 레코드를 마주한다. 때때로 복잡한 클라이언트/서버 애플리케이션에서는 레코드 내 바이트가 여러 데이터베이스 행이나 다른 소스로부터 서버측에 모이기도 한다. 하지만 데이터가 어디서 왔든 일반 레코드의 본질은 바이트의 문자열에 불과하다는 것이다:
레코드와 관련해 새로운 내용은 없지만 그것이 (또는 그와 비슷한 것이) 서버와 객체 지향의 클라이언트 간 데이터 교환을 위한 매커니즘에서 선두적 역할을 해야 한다는 것은 일반적인 서버의 내재적 부분에 속한다. 역사상 프로그래머들은 먼저 레코드 내 각 필드를 그에 상응하는 인스턴스 변수로 해석함으로써 이 문제를 해결했다.
이러한 해석은 주로 서버로부터 레코드가 도착하는 즉시 발생했으며, 때로는 Customer에 의해, 때로는 다른 객체(예: CustomerBroker)에 의해 실행되었다. 이후 레코드는 그 목적을 실현하고, 원치 않는 부속물이 되었다.
이 접근법에는 몇 가지 단점이 있다. 객체 지향 디자인에서는 가장 의미가 맞는 클래스로 행위를 분배하는 것이 항상 중요하다. 저수준 바이트 표현(representations)을 객체로 해석해야 할 책임이 있는 Customer와 같은 business 객체 클래스는 우리가 일반적으로 "customer"에 대해 갖는 기대를 초과한다. "broker"는 좀 더 나은 후보자이지만 broker는 그것이 어디에 상주하는지, 로컬 데이터베이스인지 원격 서버인지, 바이트 표현과 객체 간의 해석과 다른 함수는 무엇인지에 대한 정보를 검색하는 데에 초점을 두어야 한다.
또한 "레코드" 객체의 역할은 너무나 수동적이어서 객체라 불릴 자격이 거의 없다. 우리는 객체가 흥미로운 행위를 갖길 원하는데 위의 객체는 비활성 데이터 구조에 불과하다. 이러한 단점들은 애플리케이션이 업데이트된 customer 객체를 서버로 다시 전송해야 할 때 정반대 방향으로 반복된다. 뿐만 아니라, 때때로 사용자는 업데이트로 진행하지 않고 customer 객체를 원 상태로 복구하도록 결정해야 한다. 인스턴스 변수는 기존 값과 업데이트된 값, 두 개의 값을 동시에 가질 수 없으므로, 이 디자인은 customer 개체로 되돌아가기에 충분한 정보를 유지하지 못한다.
레코드로부터의 객체 패턴은 레코드를 customer 객체의 정수 부분으로 만들고 레코드에 번역의 책임도 부여함으로써 이러한 단점을 강조한다. Record 객체들은 전형적인 레코드와 비슷하게 보이겠지만 훨씬 더 똑똑할 것이다. 가장 간단한 형태의 record 객체는 데이터와 레코드 내부의 레이아웃, 그리고 바이트와 객체 간 해석이나 규칙을 캡슐화한다. 심지어 Customer 객체는 인스턴스 변수를 필요로 하지도 않는다. 물론 getter와 setter 메서드는 필요로 하지만 이는 인스턴스 변수 대신 기본이 되는 레코드와 직접 통신이 가능하다. 디자인은 아래와 같은 모습일 것이다:
"매핑 정보"는 #address와 같은 필드명을 바이트 배열 내 위치와 길이로 매핑하는 데에 필요한 bookkeeping으로 구성된다. "변환 정보"는 레코드의 각 필드를 문자열이나 정수와 같은 기본(primitive) 객체들로, 또는 그러한 객체들로부터 변환 시 필요한 정보를 의미한다.
이는 기본 패턴을 요약한 것이다. 세부 내용이 궁금한 독자를 위해 Record 객체와 그 협력자(collaborators)를 자세히 살펴볼 것이다. 우리는 원본 미가공 데이터를 포함하는 ByteArray뿐만 아니라 변환이 발생 시 각 필드 변환의 결과도 캐시(cache)할 수 있다. 캐시는 두 번째로 필드에 접근할 때 다시 변환해야 하는 수고를 덜어준다. 또한 업데이트된 값이 Record>>at:put:을 통해 저장되도록 유리한 슬롯을 제공한다. 사용자가 업데이트를 행할 경우 캐시된 값은 본래 바이트 배열로 복사되지만, 사용자가 되돌아가거나 역행하면(roll back) 캐시된 값은 간단히 삭제된다.
따라서 좀 더 완전한 디자인은 다음과 같다:
비객체 지향 서버를 수반하는 클라이언트/서버 애플리케이션에 이 패턴의 사용을 고려해야 한다. 이 패턴은 domain 객체를 서버 데이터 구조로부터 분리시키고 객체로의 혹은 객체로부터의 변환 오버헤드를 최소화한다. 기본 형태는 여러 실용적인 방향으로 꾸밀 수 있다. 예를 들어, getters와 setters의 생성을 자동화하거나, 레코드를 정의하는 것이 무엇이든-COBOL 카피북(copybooks), C 구조 또는 SQL문-그로부터 레코드 맵을 자동 생성할 수 있다. 그리고 인스턴스 변수가 없다는 사실이 애통하다면-객체의 핵심은 그것이 데이터를 어떻게 저장하는냐는 상세한 내용이 아니라 그 행위에 있기 때문에 그럴 필요가 없다-캐시로 사용하기 위해 부활시킬 수도 있다; 개념적으로 이것은 record 객체로부터 캐시를 business 객체로 이식하는 셈이다.
언젠가 레거시 관계형 데이터베이스가 사라지고, Object Management Group의 분산에 관한 명세(CORBA) 또는 객체 데이터베이스(GemStone, ObjectStore, Tensegrity, Versant)와 같은 기능을 통해 객체 지향의 분산 컴퓨팅이 흔해지면, 이 패턴은 더 이상 쓸모가 없을지도 모른다. 그때까지 레코드로부터 객체 패턴은 네트워크상에 있는 컴퓨터들 사이에 빠르고 명확하게 정보를 교환하는 방식으로 남을 것이다.
프록시와 고스트, I
프록시는 실제적인 것을 대신한다. 레코드로부터의 객체와 마찬가지로 프록시 역시 클라이언트/서버 디자인에서 발생한다. 그 이유는 객체 또는 그보다 더한 객체의 컬렉션이 꽤 규모가 클 가능성이 있어 서버로부터 전체 객체 또는 컬렉션을 실체화하는 것은 비실용적이기 때문이다. 대신 클라이언트는 객체에 대한 프록시를 조작하는데, 사용자가 전체 객체를 필요로 해야만 애플리케이션은 그것을 실체화한다. 이제 객체가 마침내 실체화하면 그것의 프록시는 둘 중 한 가지 방식으로 행동할 것이다. 하나는 객체로 메시지를 전송하여 사실상 프록시가 투명하게 만드는 방법이고, 아니면 스스로를 객체로 변형(또는 "morph")시켜 프록시가 사라지도록 하는 방법이다. 다음 절에서는 두 번째 유형의 프록시를 논하겠다.
첫 번째 프록시는 종종 핸들-바디(handle-body)라고 불리는데, 모습은 아래와 같다:
"핸들"은 프록시고 "바디"는 고객이다. 속이 빈 원은 프록시가 0 또는 하나의 customer 객체를 갖고 있는데, 이는 기본이 되는 customer 객체가 실체화되었는지 여부에 따라 좌우됨을 나타낸다.
❏ CustomerProxy>>address 메서드에 코드는 무엇을 하는가?
해답: Customer가 실체화되었는지 검사해야 하고, 실체화되지 않았다면 실체화해야 한다. 그리고 난 후 아래와 같이 Customer상으로 address 메시지를 전송해야 한다:
address
"Answer my address"
customer isNil
ifTrue: [customer := "materialize the customer"].
^customer address
인스턴스 변수를 당신이 필요로 함을 발견하는 순간에 초기화하는 트릭은 느긋한 초기화라고 알려진다. 클라이언트가 대부분의 customers를 필요로 하지는 않아 실체화를 위한 공간과 시간이 상대적으로 적은 customers에게 발생하길 바라는 사람도 있다.
마지막으로, 프록시는 어떻게 실체화에 올바른 customer를 아는가? 이는 한 가지 결정적인 이야기, 즉 customer가 프록시임을 확인시켜주는 키(key)도 알아야 한다. 이 키는 일반적으로 서버 데이터베이스에 각 customer로 저장된 유일한 식별 숫자다. 전체 디자인은 다음과 같다:
최종적으로 추상 클래스에 subclassResponsibility 메서드를 추가해야 한다는 사실이 기억난다. 스몰토크에서 의무는 아니지만 프로그래머가 각 서브클래스에 구체적 오버라이딩 메서드를 제공해야 함을 나타낼 때 사용하기에 훌륭한 실습이 된다 (64 페이지).
프록시와 고스트, II
두 번째 프록시 유형은 고스트라고 불린다. 아래 다이어그램을 살펴보자:
여느 프록시든 고스트는 적어도 인스턴스 변수 id에 그것이 나타내는 객체를 유일하게 식별하기에 충분한 정보를 포함해야 한다. Object>>doesNotUnderstand:는 객체가 메시지를 이해하지 못할 때마다 실행되는 익숙한 오류 메시지다. 고스트 패턴의 핵심은 고스트가 이 오류를 오버라이드한다는 데에 있다.
❏ 앞절에서 핸들-바디 프록시와 달리 고스트는 address getter를 지원하지 않는다. 그렇다면 address 메시지를 수신하면 어떻게 할까?
해답: 보통 상속된 doesNotUnderstand: 가 실행될 것이며, walkback이 잇따를 것이다. address 메시지에 좀 더 만족스러운 응답을 제공하려면 customer를 실체화시킴으로써 이 버전의 doesNotUnderstand:가 시작될 것이다. 이제 customer 객체를 이용해 전형적 핸들-바디 프록시는 customer로 address 메시지를 재전송한다.
하지만 이것은 스몰토크와[3] 관련되기 때문에 좀 더 특별한 것을 해보고자 한다: 고스트를 customer로 모핑(morph)할 것이다. 즉, 조금 전 ghost 객체가 완전히 다른 종류의 객체, customer 객체가 되는 것이다. 스몰토크에서 객체를 모핑하기 위해서는 become:로 명명된 특수 메서드가 필요하다. 마지막으로 address 메시지를 self로 재전송할 것인데, 이는 지금쯤이면 ghost가 아니라 customer를 참조할 것이다.
메서드의 전체적인 모습은 아래와 같다:
doesNotUnderstand: aMessage
"I, as a ghost, do not support aMessage. I will materialize a
customer, morph myself to it, and then try aMessage again."
|customer|
customer := "materialize the customer"
self become: customer.
^aMessage sendTo: self. "re-dispatch!"
마지막 행의 재전송이 aMessage를 어떻게 객체로서 처리하는지 주목하라. 스몰토크에서는 메시지를 포함한 모든 것이 객체다. 다른 dialects에서도 재전송 형태는 조금씩 다를 수 있지만 근본적인 사실은 그대로다: 메시지도 객체다.
전체적인 패턴은 다음과 같다:
이 모든 활동에서 바람직한 결과는, 한 때 아무 것도 이해하지 못했던 ghost가 있었던 곳에 이제 address, name은 물론이고 다른 모든 customer 메시지도 이해하는 customer가 위치한다는 것이다. 따라서 추후 모든 메시지 전송은 customer에 의해 즉시 성공적으로 처리될 것이다.
기술 노트: 이렇게 매력적인 디자인에 한 가지 단점이 있다는 점을 알려두는 것이 공평하겠다. 객체가 객체 포인터의 테이블을 통하지 않고 다른 객체로 직접 참조하는 일부 스몰토크 구현부에서는 become:의 속도가 당신이 익숙해져 있는 가상의 순간 메서드 실행에 비해 훨씬 느릴 수 있다. 이러한 속도 저하는 가상 머신이 ghost로의 모든 참조를 위치시키고 customer를 향하도록 리셋해야 하기 때문에 발생한다. 따라서 이 패턴을 이용하기 전에는 간단한 성능 검사를 실행해야 한다.
의존성 (방송, 모델-뷰, 관찰, 발행-구독이라고도 알려짐)
잘 디자인된 사용자 인터페이스에서는 뷰가 모델을 직접 알고 있지만 모델은 뷰를 알지 못한다는 사실을 기억하는가. 모델이 할 수 있는 일이라곤 그것의 뷰에게 (또는 좀 더 일반적으로는 그것의 종속자에게) update 메시지를 방송하는 것이며, 그 후 뷰는 적절하다고 간주되는 모델에게 특정 문의(inquiry)를 전송한다. 이렇게 한쪽으로 치우친 통신은 객체 지향 시스템에서 자주 발생하여 관찰자 또는 의존성 패턴이라 불리는 디자인 패턴으로 인식(recognition)을 보장한다. 가장 익숙해 할 모델-뷰 상황에서 뷰는 관찰자 또는 종속자이며, 모델은 주체(subjects)라 불린다.
이 패턴의 기본 시나리오는 다음과 같다:
제 11장에서 우리는 의존성 관계를 구현하는 다양한 방법을 논한 바 있다. 모델이 인스턴스 변수를 감추었을 수도 있고, 아니면 시스템에 모든 시스템의 종속자들을 유지하는 곳에서 공유하는 dictionary가 있을지도 모른다. 구체적인 구현부는 이 논의에서 중요하지 않다. 어떤 수단을 이용하든 뷰는 update 메시지를 수신한다.
❏ update는 어떤 일을 해야 하는가?
해답: 이는 모델에게 그것이 관심을 둔 값을 문의하고, 변경되었을지 모르는 값들을 대략적으로 아래와 행을 이용해 디스플레이에 반영해야 한다:
update
"Obtain current data, then redisplay myself"
model getValues "and process them".
self repaint.
그 결과는 다음과 같다:
제 11장에서 학습하였듯, 이 패턴의 본질적인 이익은 모델이 뷰의 종류나 수와 상관없이 기능한다는 점이다. 그러면 모델은 사용자 인터페이스의 고려 사항에 부담을 갖지 않고 문제의 개념적 객체를 표현할 수 있다. 반면 뷰는 정보를 렌더링하는 데에 집중한다. 뷰는 모델을 알고, 모델로부터 그들이 관심을 갖고 있는 정보를 얻는 방법을 안다.
"관찰자"는 종속자 또는 뷰의 부적절한 명칭이기도 한데, MVC 컨트롤러의 추가 책임을 흡수한다는 데에서 (제 11장) 오늘날 뷰들은 읽기전용(read-only) 객체에 불과하지 않다. 사용자 입력에 응답하고 그들의 모델에 값을 설정할뿐 아니라 값을 얻기도 한다. 이러한 "관찰"이 얼마나 거슬리든 모델은 자신의 뷰를 감지하지 못한 채로 남으며, 이것이 바로 패턴에서 가장 중요한 주제가 된다.
일반적으로 의존성 패턴은 그 수와 종류가 알려지지 않은 다른 객체들에게 (그것의 종속자들) 상태에 대한 변경 내용을 알리는 훌륭한 방법이다. 예를 들자면, traffic light(신호등) 시뮬레이션이 모든 vehicle에게 초록불로 바뀌었음을 알리는 것이다. 아니면 palace vault(궁전 귀중품 보관실)의 경우, 그 엔트리가 위반되면 다양한 security devices(보안 장치)와 station(기관)에게 경고를 방송하는 것과 같다. 사실 운영체제가 윈도우에게 마우스 이벤트가 발생하였음을 알리는 것과 같은 이벤트 통지의 개념이 이 패턴 방식에 들어맞는다. (또한 어떤 정보는 이 모든 방송을 이용해 argument로 전달하는 것도 합리적인데, 마우스 이벤트가 발생하는 곳의 좌표, 또는 "이 이벤트는 심각한 비상사태입니다,"와 같은 심각성의 표시도 이에 해당하는 예가 되겠다.)
패턴의 가장 분명한 형태에서 방송은 임의로 된 메시지 클러스터로 구성되는데, 클러스터는 서로 다른 이벤트에 의해 트리거된다. 패턴의 이러한 형태에 대한 논의는 120 페이지를 참고한다.
솔리테르 (싱글톤이라고도 알려짐)
컴퓨터는 어떤 객체 유형이든 하나 이하의 인스턴스를 가져야 한다. 그 예로, 창, 메모리, 또는 시간과 같은 자원을 관리하는 객체를 들 수 있겠다. 또 분산형 또는 클라이언트/서버 애플리케이션의 경우, 서버로부터 객체를 얻어 이미 얻은 객체를 기록하는 브로커(broker) 객체를 예로 들 수 있다.
문제는 클래스의 인스턴스를 구성하고 그로 접근하기 위한 프로토콜을 두 번째 인스턴스를 부주의로 구성할 가능성을 최소화시키는 방향으로 디자인하는 것이다. 가령, 아래와 같은 해결책은 바람직하지 못하다:
TheBroker := Broker new.
...
TheBroker getObjectWithId: '1234'.
우선 우리는 전역 변수 TheBroker를 소개했는데, 전역 변수를 선택하는 것은 보통 바람직하지 못하다. 전역 변수는 애플리케이션 다른 곳에서 작성하도록(writing) 유혹한다.
OnlyBroker := Broker new.
위의 코드는 두 번째 broker를 생성하거나,
TheBroker := Broker new.
그만큼 비참한 상황, 즉 위와 같이 첫 번째 broker와 서버로부터 이미 얻은 객체들에 대한 지식까지 손실되는 상황을 야기한다. 솔리테르 패턴은 전역 변수와 그 위험을 제거한다:
❏ 서버로부터 객체를 얻으려면 무엇을 작성해야 하는가?
해답:
Broker instance getObjectWithId: '1234'.
instance라는 클래스 메서드에서 느긋한 초기화는 이 요청이 처음이든 두 번째든 상관없이 Broker 의 동일한 인스턴스를 보장해준다. 우연히 생성되는 brokers를 예방할 수 있는 추가 조치로 new 메서드가 비활성화되었음을 주목하라.
일반적인 클라이언트/서버 시스템은 객체의 클래스를 하나 이상 수반한다. 객체의 각 클래스를 실체화하는 로직이 다르거나 서로 다른 클래스마다 다른 서버를 필요로 할 수 있으므로 보통은 각 클래스마다 구분된 broker를 정의하는 것이 적절하다. Broker는 CustomerBroker와 AccountBroker와 같은 특정 broker 솔리테르를 서브클래스로 갖는 추상 클래스이다.
기술적 여담: Broker에 서브클래스가 있을 경우 theOne에 대한 클래스 변수는 스몰토크의 클래스 인스턴스 변수만큼 적절하지 않다. 각 서브클래스는 서브클래스의 구분된 broker가 위치할지 모르는 슈퍼클래스의 클래스 인스턴스 변수에 대해 자신만의 구분된 비공유 복사본을 상속한다. 클래스 변수는 이러한 프로퍼티를 갖고 있지 않다. 대신 모든 서브클래스는 슈퍼클래스에서 정의한 클래스 변수를 갖고 있다-고유의 구분된 broker를 갖고 있다면 그다지 유용하진 않다.
듀엣 (파드되, 이중 전달이라고도 알려짐)
이처럼 "함수"가 단일 객체 상의 메서드처럼 보이지 않고 두 개의 동료 객체 상의 "메서드"처럼 보이는 드문 경우들이 객체 지향 프로그래밍의 전체적인 적용 가능성에 의혹을 불러일으킬 수도 있다. 세상에 대한 우리의 현대적 객체 중심의 관점을 이러한 상황에 적용하기엔 너무 편협하다.
예를 들어, play 메서드는 Musician을 argument로 취하는 Instrument 클래스에 있을까? 아니면 Instrument를 argument로 취하는 Musician 클래스에 있을까? 아니면 한 쌍의 객체에서 작동하는 무언가에 해당하는가? 한 쌍의 객체에서 연산을 실행해야 하는 상황이 오면 우리는 듀엣 패턴을 이용한다. 이 패턴의 가장 흥미로운 적용은 Instrument와 Musician이 모두 서브클래스를 가질 때 발생하는데, play가 두 개의 arguments 모두에서 다형적인 연산이 되기 때문이다. (예제와 논의는 170 페이지에서 다중 메서드에 관한 논평을 참고하라.)
듀엣의 기본 예제는 산술로서, a + b(a는 메시지를 수신한다; b는 argument에 "불과"하다)에 대한 비대칭적 해석이 스몰토크 초보자들을 겁먹게 만들곤 한다. 초기의 실용적 학교 교육(functional schooling)에서 우리는 a + b 연산의 대칭에 대한 신뢰를 발전시켰다. 심리적으로 우리는 둘 중 하나로 메시지를 전송하기보다는 두 개의 객체에서-다중 메서드-덧셈을 연산에 포함하길 원한다. 하지만 스몰토크는 "순수" 객체 지향 언어로서, 이를 의무화할 수 없다.
상업용 객체 지향 언어들 중에 CLOS만이 진정한 다중 메서드를 지원한다. 스몰토크에서는 듀엣 패턴을 이용해 산술적 다중 메서드를 통합할 수 있다:
- <그림 속>
- "aNumber는 내가 이해하기 너무 복잡한 타입일 수 있기 때문에 그가 염려하도록 놔둘 작정이다."
- "float로서 그들이 무엇이든 더 간단한 타입을 처리할 수 있어야 한다."
- "anInteger를 나에게 추가하는 진짜 작업을 실행하라. 나는 aninteger가 참으로 Integer임을 추측할 수 있다!"
당신은 +를 이용해 integer와 float를 어떤 순으로든 추가할 수 있다. 하지만 둘 중 하나만-Float-진짜 작업을 실행할 수 있다. 나머지-Integer-에게 작업을 수행하도록 요청할 경우 자동으로 Float에게 메시지를 전송하면서 integer가 산술을 실행하는 데에 Float로부터 도움을 구함을 알린다.
이 패턴이 "듀엣"인 이유는 참가자들을 동료(peer)로 보이게 만들기 때문이다. 이중 전달이나 파드되라고 부르는 이유는 통제력의 극적인 전달, 그리고 다른 객체로 self를 argument로서 전달하기 때문이다.
패턴을 구현하는 프로그래머에게는 조건부 검사가 제거된다는 점이 중요한 결과가 되겠다. 정수는 어떤 것도 검사하지 않는다; 즉시 그리고 무조건적으로 argument에게 메시지를 전송하면서 integer가 도움이 필요함을 알린다. 다형성을 학습하면서 목격하였듯 소프트웨어 유지보수를 단순화하는 최선의 방법은 조건부가 없는 코드를 작성하는 것이다.
소비자(customer) 프로그래머에게 가장 중요한 결과는 개념적 단순화이다: 그가 두 객체 중 + 메시지를 누구에게 보내는지는 중요치 않다. 사실상 그의 관점에서 보면 +는 비객체 지향적 연산이다. 또한 연산에 어떤 타입의 객체가 참여하는지도 중요치 않다. 두 번 생각할 필요없이 Float, Integer, 또는 Fraction의 어떤 조합이든 추가할 수 있는 것이다.
❏ + 메시지가 정수와 부동소수점의 덧셈과 같은 복잡한 시나리오 대신 두 개의 정수를 덧셈하는 데에 사용될 경우 어떤 일이 발생할까?
해답: 여전히 sumFromInteger:가 무조건적으로 실행되지만 이제 Integer로 전송되어 Integer 클래스가 sumFromInteger:도 잘 구현하도록 한다. 다행히 이러한 기대는 타당하다-정수는 자신에게 또 다른 정수를 더할 수 있어야 한다.
듀엣은 산술을 구현하기 깔끔한 방법으로 VisualWorks와 그보다 덜한 VisualSmalltalk는 산술에 이 방법을 사용하지만 IBM Smalltalk는 그렇지 않다. IBM Smalltalk에서는 속도를 위해 가상 머신에서 직접 산술을 구현한다.
변호사 (객체 핸들러라고도 알려짐)
때로는 두 가지 종류가 함께 작업하면서도 서로에 대한 직접적 지식으로 인해 복잡해질 필요가 없다. 이러한 상황은 다대다 관계(217 페이지)에서 목격한 바가 있을텐데, 이제 일대일 관계에서 살펴보고자 한다.
Icon 객체와 그것이 의미하는 model 객체를 생각해보자. 모델-뷰 구분 정신에 따르면, 상징적(iconic) 표현과 같은 시각적 객체의 지식을 model 객체에게 전송하는 것은 부적절하게 보인다. 또한 icon은 간단한 시각적 객체-보통 비트맵-로서, 사용자 인터페이스가 어떻게 그것을 이용해 일부 모델 객체와 관계하는지 icon이 알 것이라고 기대하지 않는다.
우리는 두 객체를 분리하길 원하면서도 icon의 경험을 model에게 알려주길 바란다. 예를 들어, 그래픽 에디터에서 사용자가 그래픽 요소를 나타내는 아이콘을 다른 위치로 드래그할 경우, 그래픽 요소의 좌표가 변경될 것으로 예상한다. 이러한 책임은 아이콘이나 그래픽 요소 중 하나에게 전적으로 부여된 것이 아니다. 따라서 우리는 아이콘과 그래픽 요소를 모두 알고 있는 세 번째 객체, 객체 핸들러 또는 변호사(lawyer)라 불리는 객체를 구성한다 [Collins 1995].
이후 사용자 인터페이스는 아이콘 대신 lawyers를 조작한다. Lawyers를 통해 사용자 인터페이스는 lawyer가 나타내는 대상이 아이콘이든 기본이 되는 객체든간에 그 대상과 간접적으로 통신할 수 있다. 패턴의 모습은 아래와 같다:
그래픽 요소의 좌표를 업데이트하는 것은 한 lawyer-아이콘과 그래픽 요소 간을 조정하기만 하는 lawyer-의 상대적으로 간단한 기능이다. 이 lawyer는 중재자(mediator) 패턴의 일례다 [Gamma et al. 1995]. Lawyer는 다른 lawyers로 이야기함으로써 더 유용해진다; 이것이 우리가 다음으로 살펴볼 관계다.
아이콘을 다시 고려해보자. 아이콘을 다른 아이콘으로 드래그한 결과로 발생하는 일반적인 시각적 피드백은 아이콘이 표현하는 객체의 타입에 따라서만 좌우된다. 예를 들어, 피드백은 당신이 파일 아이콘을 계산기 아이콘에 드롭할 수는 없지만 프린터 아이콘에 드롭할 수는 있음을 알릴 것이다. 사실 이러한 점은 기본이 되는 객체와는 무관하다; 사용자 인터페이스는 오로지 아이콘으로부터 온 피드백을 계산할 수 있다. 하지만 좀 더 정교한 사용자 인터페이스가 좀 더 세련된 피드백, 즉 printer 객체가 오프라인 일 경우에는 프린터 아이콘에 파일 아이콘을 드롭할 수 없다는 표시와 같은 피드백을 제공해야 한다고 가정해보자. 이러한 피드백은 기본이 되는 printer 객체의 상태에 따라 달라지므로 오로지 아이콘에서 비롯된 피드백으로 결정할 수 없다.
❏ 어떤 아이콘이 이러한 결정을 내릴 수 있는가?
해답: Icon 객체들은 연관된 객체의 상태를 의식하지 못하기 때문에 답에서 제외되어야 한다. 하지만 lawyer 객체들은 아이콘(파일 아이콘이 프린터 아이콘 위에 있는가?)뿐만 아니라 기본이 되는 객체에 (프린터가 온라인 상태인가?) 관해 알고 있기 때문에 이러한 결정을 내릴 수 있다. 따라서 파일 lawyer는 적절한 피드백을 결정하기 위해 프린터 lawyer와 협상을 할 수 있다. 그 관계와 책임은 다음과 같다:
상호작용을 살펴보도록 하자. 사용자가 파일 아이콘을 프린터 아이콘 위에 위치시켰다고 가정하자. 사용자 인터페이스는 아이콘보다 lawyer를 조작하기 때문에 파일 lawyer에게 그것을 프린터 laywer에 드롭 가능한지 물을 것이다. 질문에 답하기 위해 파일 lawyer는 프린터 lawyer에게 그것이 유효한 타겟인지를 질문하면 프린터 lawyer는 프린터가 온라인인지 프린터에게 질문함으로써 답을 결정한다. 그 응답에 따라 사용자 인터페이스는 "드롭할 수 없습니다," 또는 "드롭해도 좋습니다,"라는 지시를 표시할 것이다.
협상을 두 개의 lawyer로 위임하지 않고 우리는 네 개의 주요 당사자들-두 개의 아이콘과 두 개의 모델 객체-간에 직접 전송되는 메시지를 작성하고 유지해야 할 것이다. 윈도우식 사용자 인터페이스 디자인에서 관찰자 또는 모델-뷰의 구분이 상호작용을 간소화하듯이 lawyers는 직접 조작 사용자 인터페이스 디자인에서 상호작용을 간소화한다.
이 패턴은 전적으로 사용자 인터페이스에 관한 것만은 아니다. 여기서 필수적 교훈은 두 객체가 서로 일대일 연관에 있을 때마다 연관은 장래의 (prospective) 타당한 객체가 된다는 점이다. 이 객체-lawyer-는 연관을 구체화한다. 우리 예제에서 lawyer는 아이콘과 그것이 나타내는 객체 간 연관을 구체화한다. 예제에서 한 가지 더 놀랄 만한 전개는 두 lawyer가 서로 간 협상으로 넘어갈 수 있다는 점이다.
복합체(composite)
복합체 객체는 재귀적 관계를 잘 보여준다. 그래픽을 중첩하든, 재료표를 폭발하든 아니면 파스 트리를 처리하든, 복합체는 구조 내 모든 객체나 노드로 특정 연산을 적용하도록 해준다.
망원경과 같은 복잡한 제품의 가격을 계산하길 원한다고 가정하자. 망원경은 기본 영역(scope)과 접안 렌즈로 구성된다. 기본 영역은 튜브와 대물렌즈로 구성되고, 대물렌즈는 유리와 정교한 형석 코팅으로 구성된다...이러한 제품은 아래와 같은 재귀적 디자인으로 표현할 수 있겠다:
이 그림은 복합 제품이 다른 제품들로 구성되고, 이러한 제품은 원자 구조(atom)이거나 다른 복합 제품(compound)이 될 수 있음을 보여준다. Product는 추상 클래스다. 이는 그 비용을 계산할 수 없고 인스턴스도 갖고 있지 않다. Atom도 주로 추상 클래스다; 우리 예제에서는 FluoriteCoating과 같은 더 이상 축소가 불가한 서브클래스들을 갖고 있는데 이 서브클래스 각각은 고유의 비용을 계산할 수 있다.
❏ Compound>>cost 에 해당하는 스몰토크 코드는 무엇인가?
해답: 인스턴스 변수 products는 컬렉션이므로 (예: OrderedCollection), cost는 컬렉션 내 각 제품을 반복하여 그 가격을 요청하고, 총계를 증가시켜 그것으로 응답한다. 따라서 전체적인 패턴은 다음과 같다:
복합체 패턴은 단순하고 우아하다. 그 매력을 설명하는 데에 도움이 될만한 점은, 다형적인 cost 메서드가 각 클래스마다 다르지만 여전히 의심할 여지없이 훌륭한 방식으로 행동한다는 점이다.
복합체 패턴은 다음에 소개할 패턴을 비롯해 특수화된 패턴들의 기반이 된다.
방문자 (visitor)
방문자 패턴은 앞의 두 패턴, 듀엣과 복합체의 특징들을 섞은 패턴이다. 이 패턴은 각 노드에서 실행되어야 하는 액션이 위의 cost처럼 간단한 다형성 메시지인 경우에만 매력적이다. 액션이 지대한 영향을 미칠 다른 객체들을 수반하는 복잡한 메서드가 될 징후가 보이면 복합체 해답은 그 매력을 잃어버린다.
언어 번역이나 프로그램 컴파일과 관련된 문제들이 이 범주에 속한다. 번역기가 파스 트리의 노드를 통해 반복(iterate through)하면서 외국어에 관한 지식을 요구할지도 모른다. 이 지식은 아마 파스 트리 내부에서 캡슐화되어 있지 않을 것이다. English sentence(영문장)으로부터의 노드 객체는 그 구문적 역할을 영어로 알고 있지만 외국어로 된 단어나 구문은 알아선 안 된다.
여기 간단한 예제 문제가 있다. 필자가 학교에서 배운 문법을 소개하는 동안 조금 기다리길 바란다. 번역 프로그램이 처음으로 하는 일은 당신이 문장을 다이어그램화했을 때 했던 일과 같다. 파스 트리를 생성하는 것이다[4]. "고양이가 강아지를 쫓는다"라는 문장은 아래와 같은 파스 트리를 낳는다:
우리는 최종적으로 이 트리에서 "Le chat poursuit un chien"라는 French 문장을 생성하길 원한다. 트리의 각 노드는 객체지만 여러 클래스의 인스턴스다. 젤 밑열에 위치한 leaf 노드들은 Terminal 노드라 불린다. 따라서 Terminal의 서브클래스는 3개로, Article, Noun, Verb가 된다. Leaf가 아닌(non-leaf) 노드들은 NonTerminal 노드들이다. NonTerminal의 서브클래스로는 Sentence와 NounPhrase가 있다. 이러한 클래스에 대한 상속 계층구조는 복합체와 매력적으로 닮았다:
하지만 복합체 패턴을 적용하여 각 노드 객체에게 translate 메시지를 재귀적으로 전송한다면 위에서 언급한 결함에 직면한다. 첫째, 트리는 English 파스 트리이고, 그것의 노드들은 어떤 French도 알아선 안 된다. (혹 아는 경우, English를 가령 Chinese와 같은 언어로 번역하는 데에 디자인을 재사용할 수 없다.) 둘째, 번역은 leaf 노드를 하나씩 번역하는 것보다 더 섬세하다. 고양이(cat)라는 명사를 담소(chat)로 번역하면 바로 앞의 정관사 the에 영향을 미친다-chat의 스펠링과 성별은 (la 혹은 l'가 아니라) le라는 결과가 나와야 함을 암시한다. 불행히도 translate 메시지가 cat에 도달할 때가 되면 앞 단어는 이미 번역이 되어 있다. 올바로 번역하기 위해서는 NounPhrase 클래스를 수반해야 함이 분명하며, 복합체 패턴을 계속해서 사용할 경우 translate 메서드에 복잡하고 French 특정적인 로직을 추가해야 할 것이다.
❏ English 기반의 노드 클래스로부터 이러한 French 특정적 문제를 분해하는 방법은?
해답: translate 메서드를 구체화하는 새 클래스를 발명하라. 즉, 각 노드에 복잡한 French 특정적 translate 메서드를 작성하는 대신 Translator라는 클래스를 정의하라. Translator의 인스턴스는 각 노드를 방문한다. 각 방문 시 어떤 연산을 실행하도록 요청받는데 이러한 이유로 이 패턴을 방문자라고 부르며, 그 모양은 다음과 같다:
- <그림속-오른쪽>
- translateNounPhrase: aNounPhrase
- "French가 남성성일 경우 the에서 le 또는 l'를 생성하고 a는 un으로 해석한다. 여성성일 경우, ..."
- translateVerb: aVerb
- "dictionary에서 aVerb를 검색하여 번역하라"
번역의 의무는 초라한 노드로부터 온전한(able-bodied) 방문자에게 부여된다. Translator는 언어 특정적 번역 사전을 저장하는 인스턴스 변수를 갖게 될 것이다. 복잡한 translator 메서드는 visitBy: 메서드로 대체되는데, 이는 translator 객체로 메시지를 다시 전송하고 self를 (노드) argument로서 전달하는 일밖에 하지 않는다. 그러면 translator 객체는 노드를 이용해 원하는 것은 무엇이든 실행할 수 있다. 명사구의 관사나 성별을 해석하는 것과 같은 복잡한 로직은 파스 트리의 노드 대신 translator에 상주한다. 듀엣과의 유사점을 주목하라: translator로 전송되는 메시지는 듀엣에서와 마찬가지로 (sumFromInteger:) 전달되는 객체의 타입을 (translateNounPhrase: 또는 translateVerb:) 알려준다.
이 패턴은 다른 translator 객체들을 실행 가능하게 만들기도 한다 (EnglishToFrench, EnglishToChinese...). English 파스 트리는 전혀 변경되지 않는다; 다른 타겟 언어로 번역하기 위해선 단순히 다른 translator가 파스 트리의 노드들을 방문하면 된다. 방문자 패턴은 가장 적절한 클래스로 행위를 깔끔하게 팩토링한다. 파스 트리를 관련 없는 정보로 채우지 않기 때문에 다른 언어 번역에 재사용하기가 더 수월하다. 방문자 패턴은 이번 장에서 마지막을 장식하는데, 이는 두 가지 기본적 패턴을-복합체와 듀엣-연장하고 구체화의 가치를 다시금 보여주기 때문이다.
결론
객체나 클래스를 그룹화하는 방법은 무한하게 많기 때문에 모든 그룹화를 패턴이라고 부르는 것은 건설적이지 못하다. 그룹화는 디자이너에게 유익할 때에만 패턴이라 부른다. 무엇이 "유용성"을 결정하는지와 관련해 몇 가지 비공식적 기준을 소개하겠다:
- 그룹화는 은유(들) 또는 다른 인식 가능한 개념들을 기반으로 한다. 이 세 가지 기준은 디자이너가 패턴을 기억하도록 도와준다.
- 그룹화는 연상적이고 기억하기 쉬운 이름을 갖고 있다. 객체 지향 디자인들 중에 패턴 이름은 문제를 어떻게 해결할지에 대한 의견을 빠르게 공유하거나 소프트웨어 조각이 어떻게 구성되었는지 이해하는 데에 사용되는 공통어이다.
- 그룹화는 마이크로 아키텍처와 닮았다. 다시 말해 객체의 즉석 모임이 아니라 함수, 구조, 미학에 대한 일관된 감각을 전달하는 모임이다.
몇 가지 디자인 패턴을 알고 나면 그것을 적용시킬 수 있는 기회를 인지하기 시작할 것이다. 몇 개의 코드를 배우는 기타리스트나 오프닝(openings)을 공부하는 체스 선수에 비유할 수 있겠다. 머지않아 그것을 사용할 기회를 갖게 될 것이다.
지금까지 학습한 패턴을 요약해보겠다:
적용 가능성 | 패턴 | 예제 문제 |
큰 객체와 작은 객체 | 똑똑한 컨테이너(smart container) | 히스토리 로그(History log), 주소록(address book), ... |
중동사(Heavy verb) | 구체화 | Converters, Search, Command, ... |
대화형 애플리케이션 | 커맨드(command) | Undo/redo |
중복 코드 | 팩토리 메서드 | 하나의 클래스가 주기적으로 다른 클래스를 사용한다 |
플랫(Flat) 또는 관계형 데이터 | 레코드로부터의 객체 | 클라이언트/서버 실체화 |
너무 많은 큰 객체 | 프록시와 고스트 | 원격 계산 |
한방향 관찰 | 의존성 | MVC, 알림(alert), 이벤트, 콜백 |
하나 이하의 인스턴스 | Solitaire (싱글톤) | Brokers |
메서드 대신 함수 | 듀엣 | 다중 메서드 |
일대일 연관 | Lawyer | 상태 기반의 시각적 피드백 |
중첩된 객체 | 복합체 | 그림(Drawings), 재료표(bill-of-materials) |
언어 번역 | 방문자 | 번역기, 코드 생성기 |
똑똑한 컨테이너, 구체화, 레코드로부터의 객체, 듀엣, 변호사 방법만 제외하고 모두 [Gamma et al. 1995]에서 찾을 수 있다. 똑똑한 컨테이너에 관한 상세한 정보는 [Coad et al. 1995]; 레코드로부터의 객체는 [Wolf and Liu 1995]; 듀엣은 [Ingalls 1986]; lawyers(객체 핸들러)는 [Collins 1995]를 참고한다.
논평: 역사
소프트웨어 패턴에 대한 개념은 Xerox PARC에서 근무하던 Adele Goldberg와 Alan Kay가 초보 프로그래머들이 프로그래밍 언어의 탁월함과 상관없이 어려운 문제를 해결하지 못한다는 사실을 인지했던 1970년대 중반으로 거슬러 올라갈 수 있겠다. 초보자들은 언어를 충분히 이해할 수는 있었지만 디자인 패턴이 언어로 표현 가능한지는 알 수 없었다. 이러한 상태는 영어를 읽고 쓸 수는 있지만 위대한 문학의 지혜를 소화하기엔 너무 어린 아동에 비유할 수 있겠다. 원 (raw) 언어에 어느 정도 개념적 빌딩 블록을 제공하기 위해 Goldberg와 Kay는 디자인 템플릿(design templates)를 도입하였는데, 이는 오늘날 디자인 패턴의 선구자가 되었다 [Kay 1993].
Kent Beck과 Ward Cunningham은 1987년에 소프트웨어 패턴에 대한 작고 강력한 집합을 논하였다 [Power 1988]. 하지만 패턴에 대한 관심은 사실상 OOPSLA '91에서 Bruce Anderson의 아키텍처 소책자 워크숍에서 급성장했다[5]. 그리고 1 년 후, 패턴에 관한 논문이 나타나기 시작했다 [Coad 1992; Eggenschwilder and Gamma 1992; Johnson 1992]. 그 중 중요한 책자가 1994년 말에 소개된다 [Gamma et al. 1995]. Ralph Johnson은 1994년에 소프트웨어에서 패턴을 주제로 한 학회를 처음으로 조직한다 [Coplien and Schmidt 1995].
소프트웨어 패턴은 갑자기 너무 유명해지는 바람에 지나치게 노출되는 위험이 있다. 거의 모든 것이 은유로 구성 가능하듯 (146 페이지) 거의 모든 것이 패턴으로 구성 가능하다. 따라서 다수의 "패턴"은 일반 컴퓨팅 대중이 사용하기엔 너무 편협하고 신비로우며, 의당 흥미로운 많은 논문들이 부자연스럽게 패턴 구성 방식을 중심으로 작성되었다. 그럼에도 불구하고 아직도 소프트웨어에서 패턴에는 유망하고 탐구되지 않은 수많은 영역이 남아 있다.
프로그래머가 소프트웨어 패턴에 대해 들뜨기 전에 건축학자 Christopher Alexander와 그의 동료들은 [Alexander et al. 1977; Alexander 1979]를 시작으로 사람들에게 살아 있는 공간을 지어주는 데에 패턴을 사용하는 방법과 관련해 일련의 서적을 발행한다. Alexander는 253개에 달하는 패턴을 실은 그의 책자는 지역사회의 설계부터 방의 장식을 결정하는 일까지 모든 방면에서 기반이 될 수 있다고 제시했다. 훌륭한 소프트웨어 패턴과 마찬가지로 그의 패턴은 은유적이고 ("시티 카운티 손가락들"), 기억하기 쉽고 ("모든 방마다 두 벽만 보라"), 건축적이다 ("주변을 비춘다"). 그의 작업은 초창기 소프트웨어 패턴 연구원들을 고무시켰다.
Alexander는 증명된 패턴을 건축의 행위에 적용시키는 것 이상을 원했다. 그는 결과적으로 살아 있는 공간에 그가 "이름 없는 특성(quality)"이라 부르는 것이 있기를 바랐다. 패턴은 이러한 결과에 도달하기 위한 수단이었을 뿐이다. 불행히도 이러한 선종과 같은(zen-like) 특성은 어떠한 정의도 허용하지 않았다. 대체로 사람들은 소수의 주택과 공동체에만 특성이 있으며 대부분은 그렇지 않다고 동의했다. 소프트웨어에서도 마찬가지다. 프로그래머들은 대부분 어떤 소프트웨어가 원숙하게 디자인되었음을 동의하지만 그러한 소프트웨어를 보는 일은 드물다.
패턴에 관한 Alexander의 저서가 출판되고 몇 년이 지나 그는 패턴만으로는 "이름 없는 특성"이 드러난 결과를 보장하기가 무리임을 깨달았다. (1977년에도 그런 사실을 알고 있었지만 비효율성의 정도는 실망스러운 실험을 행한 후에야 나타났다.) 체스의 비유로 다시 돌아가보자: 체스 패턴을 이해하고 적용하면 선수의 게임은 향상되나 아름다운 체스 게임이 발생하는 경우는 드물다. 소프트웨어 이러한 특성을 얻기 위한 열쇠는 (또는 어떤 노력이든) 아직도 발견되지 않았다. 수사학과 철학에 관련해 문제의 깊이를 인식하기 위해서는 [Pirsig 1974]를 읽길 바라며, Alexander가 아키텍처에서 문제를 어떻게 해결하길 원했는지, 그의 의견이 소프트웨어에 어떤 의미를 지니는지에 관해서는 논문을 참고한다 [Gabriel 1993-1994].
Notes
- ↑ 심지어 세계 챔피언 Gary Kasparov를 상대로 한 게임에서 우승한 체스게임 슈퍼컴퓨터 Deep Blue조차 raw computational power를 주장하기 위해 "패턴" 을 사용한다. 예를 들어, 컴퓨터는 "캐슬링된(castled) 킹이 그렇지 않은(uncastled) 킹보다 더 큰 보호를 받는다." 는 등의 휴리스틱을 사용할 수 있다. 휴리스틱(Heuristic)은 경험에 의거한 법칙으로서, 이 또한 패턴을 생각하기에 나쁜 방법은 아니다.
- ↑ 어떤 사람들에게는 패턴이 훨씬 더 많은 것을 의미 또는 암시하기도 한다. 더 깊이 이해하기 위해서는 이번 장 마지막에 실린 논평을 (240 페이지) 참고한다.
- ↑ 이번 장에서 유일하게 스몰토크에 특화된 패턴이다.
- ↑ 파스 트리를 생성하는 방법에는 여러 가지가 있는데 가장 간단한 방법은 재귀적 하향식(recursive descent)으로 알려진다. 이 방법 또는 다른 방법을 이용해 파스 트리를 생성하는 기법은 여기서 관련이 없다; 우리는 파스 트리를 갖고 난 후에 무엇을 할 수 있는지에 관심이 있다.
- ↑ OOPSLA 는 "객체 지향 프로그래밍, 시스템, 언어, 그리고 애플리케이션" 에 관해 열리는 연례 회의다.