SmalltalkObjectsandDesign:Chapter 07

From 흡혈양파의 번역工房
Jump to navigation Jump to search
제 7 장 CRC 카드

CRC 카드

이제 컨테이너와 객체 정체성의 기본 개념으로부터 구체적인 디자인 문제, 그리고 그것을 해결하는 데 도움이 되는 몇 가지 기법들로 이동하겠다. 여기 실린 디자인을 작업한 후 제 8장 에서는 그것을 구현하는 스몰토크 코드를 작성할 것이다.


객체 지향과 관련된 주제 중에서 객체 지향 디자인 방법만큼 논쟁을 불러일으키는 주제는 아마 없을 것이다. [1]무엇이 훌륭한 디자인 방법에 기여하는지에 대한 질문은 광범위하면서 흥미롭지만 이 책의 중점은 아니다. 그보다는 애플리케이션을 설계하는 데에 피할 수 없는 한 가지 단계, 즉 클래스를 발견하는 단계를 도와주는 간단한 기법을 제시하고자 한다. 이 기법은 1989년 Kent Beck과 Ward Cunningham이 발표한 개념으로, CRC 카드를 단순화시킨 것이다. 이후부터 CRC 카드 또는 클래스 - 책임 - 공동 작업자 카드는 널리 사용되고 모방되어왔다. (이번 장 마지막에 실린 논평을 참고하라.)


3 x 5 인치 또는 4 x 6 인치 중 마음에 드는 색인 카드로 시작하라. (Kent와 Ward의 연구에는 4 x 6인치라고 적혀있지만 정작 그들은 따르지 않는다.) 색인 카드가 없다면 여러 장의 전장 시트(full sheet)를 1/4로 찢으면 되겠다. 각 카드에 객체의 인스턴스 그림을 그려라:


Chapter 07 01.png

클래스의 인스턴스가 가져야 하는 "노하우", 즉 인스턴스가 어떤 기능을 가질 것인지에 대해 생각하는 바를 책임(responsibility)으로 기록하라. 유용하다고 생각되는 상세(detail) 수준에서 기록하라. 예를 들어, 먼저 traffic light 객체의 책임을 다음과 같이 기술하길 원한다고 치자: "교차로에서 차량의 흐름을 질서 있게 중재하라." 후에 코드를 작성할 시기가 다가오면 책임을 좀 더 구체적으로 설명할 수 있다: "advanceColor," "initializeGreenDuration:," 등. 무엇을 하든 카드를 독단적으로 사용하지 말라. 이는 비형식적 툴이며, 당신의 창의성을 제한하는 것이 아니라 고무시켜야 한다.


자신이 작성한 내용이 마음에 들지 않는다면 카드를 버리고 다시 시작한다. 카드가 비싸지 않기 때문에 처분 가능성이 장점 중 하나가 된다. 초기단계에서는 실수를 행하더라도 별로 손해 볼 것이 없다; 어리석은 생각일수록 초기에 버리는 것이 더 나은 디자인을 만드는 데에 더 유익하다. 이는 인류에게, 심지어 가장 성공한 사상가에게도 영원한 사실이다. DNA 의 이중 나선 구조를 발견하여 노벨상을 수상한 Francis Crick은 "칭찬받을 만한 일을 굳이 꼽자면…아이디어를 유지할 수 없게 되었을 때 그 아이디어를 기꺼이 버릴 준비가 되었다는 데에 있습니다,"고 말했다 [Crisk 1988]. 어느 정도 막다른 길을 살펴보지 않는 소프트웨어 디자이너는 많이 학습하지 못할 것이다.


CRC 카드의 또 다른 장점으로는 유형성을 들 수 있다. 디자이너는 CRC 카드를 강조하기 위해 흔들어 메시지의 argument로서 이동시키거나, 관계를 설명하기 위해 테이블에 배열한다. 이러한 유형성은 CRC 카드를 컴퓨터화된 툴로 표현하려는 시도가 왜 기대에 미치지 못했는지를 설명한다. CRC 카드를 컴퓨터에 묻으면 사용자 인터페이스가 얼마나 세련되었는지와 상관없이 상상력과 브레인스토밍을 가능한 한 자극해야 할 때 오히려 당신의 사고를 제한한다. 체계적인 사고를 위한 시간이 늦게 찾아온다.


디자인 연습

❏ CRC 카드를 이용해 당좌 예금 계좌(checking account)를 추적할 수 있는 간단한 개인 컴퓨터 애플리케이션을 디자인하라. 거래는 로그 또는 레지스터에 저장되고, 일자별로 정렬된다.


여러 경쟁적 대안책을 살펴보고 가장 유망성이 적은 카드는 버릴 것을 기억하라. 다음 장에서는 이 연습을 위한 코드를 작성하여 디자인을 간단하게 유지할 것이다-모든 것을 갖춘(full-blown) 애플리케이션이 필요로 하는 상세 데이터 유형으로 한정하지 말고, 온라인 확인과 같은 확대(extensions)에 너무 열중하지 말라. 두 장에서 다섯 장의 카드 범위를 목적으로 하라. 마지막으로, 제 6장의 교훈을 기억하라: 컨테이너는 거의 모든 객체 지향 디자인에서 필수 참여자다.


해결책과 논의

문제가 막 발생하기 시작했다면 문제가 되는 기술(description)에서 명사와 동사를 살펴보라. 명사는 객체의 지원자(candidate)이고 동사는 책임 또는 메서드의 지원자이다. 이는 권장사항이지 일반 규칙은 아니다. (예를 들어 15장에서 동사는 객체에 대해 훌륭한 지원자가 될 것이다. 디자인은 항상 현재 문제에 의존한다.)


문제가 되는 문(statement)에서 다음과 같은 명사들이 가능성이 높다: account, log, transaction, date. 앞에서 우리는 이미 Date 클래스를 스몰토크에서 사용할 수 있다는 사실을 살펴보았다. 나머지 세 개의 클래스에 집중하고, 그에 실행 가능한 책임부터 시작해보자:

Chapter 07 02.png


Account balances와 transaction amounts는 문제가 되는 문에서 명시적이진 않지만 피할 수 없는 요소처럼 보인다. 반면, 부수적 요소들-계좌 번호와 설명, 수표 번호, 수취인, 메모-은 우리가 추구하는 본질적 객체 상호작용의 이해에 기여하는 바가 거의 없다는 사실을 바탕으로 생략하기로 결정했다. 그러한 정보를 추가하면 간단하고 정확하긴 하지만 주의를 분산시킨다. 필자는 "계좌가 로그를 보관한다,"는 책임에서 암시하는 밀접한 협력을 강조하기 위해 Account에 Log 카드의 모서리를 덮어 그렸다는 점을 주목하라.


여기까지 transaction 객체들은 특별하지 않다. 이는 변화에 관한 것이다. 로그의 가장 중요한 책임인 '정렬'은 제 6장에서 정확히 이러한 책임을 이용해 작업한 클래스를 상기시킨다. 제 6장의 클래스는 SortedCollection였다 (71 페이지). 따라서 새로운 Log 클래스를 정의하기보다 SortedCollection 클래스를 재사용할 것이다. sortBlock의 의미를 고려할 때 정렬된 컬렉션 객체들은 그것으로 추가되는 객체들을 정렬한다는 사실을 기억해야 한다. 기본 sortBlock은 객체들이 <= 메서드를 이해한다고 가정한다. 그것이 바로 정수와 그에 가까운 대상들(kin)이 자동으로 정렬 가능한 이유이다. Transactions가 기본 sortBlock을 통해 정력 가능하도록 보장하기 위해서는 그들 또한 <=를 통해 비교 가능해야 한다. 따라서 위의 transaction 카드에 대한 추가 책임은 "서로 비교 가능"이 되겠다.


최종 구현을 바라보면서 관찰 내용을 좀 더 상세한 스몰토크 편향적인 카드 버전으로 요약할 수 있다:[2]

Chapter 07 03.png


SortedCollection을 둘러싼 점선은 Date와 마찬가지로 카드나 클래스가 이미 스몰토크에 존재하기 때문에 새 카드나 클래스를 보장하지 않음을 의미한다. 따라서 Account와 Transaction 두 개의 클래스만 작성하면 되겠다. 메서드가 argument로서 기대하는 객체의 유형은 시각적 편의를 위해 그림에서 생략하였는데, 아래와 같다:

  • handleTransaction: aTransaction
  • initialBalance: anInteger
  • initialAmount: anInteger date: aDate
  • <= another Transaction
  • add: aTransaction
  • remove: aTransaction


anInteger는 달러와 같은 화폐량을 나타낸다.


이제 상속이나 집합과 같은 관계로 돌아가자. 상속은 이 디자인에서 클래스와 무관하지만 집합은 중요하다. CRC 디자인의 책임을 "아는 것"과 "유지하는 것"을 구현하는 일반적인 방법은 인스턴스 변수의 정의를 통해서이다. 따라서 책임에 대해 "계좌는 그 잔고를 안다,"는 Account 에서 balance라는 이름의 인스턴스 변수를 정의하여 계좌의 현재 잔고를 보유하도록 한다. 게좌가 "로그를 유지하도록" 만들기 위해서는 정렬된 컬렉션을 참고하게 될 인스턴스 변수 log를 정의하라. 이와 유사하게, transactions는 금액과 일자를 "알기" 때문에 amount와 date 인스턴스 변수를 가져야 한다.


"transactions를 포함해야 하는" 로그의 책임은 어떤가? 우선 스몰토크가 우리를 위해 이미 구현해놓은 SortedCollection 클래스를 재사용할 예정이기 때문에 사실상 이 책임을 이행하기 위한 상세(details)는 염려할 필요가 없다. (그럼에도 불구하고 정렬된 컬렉션과 같은 컨테이너의 내부에 관해 할 말이 있다. 컨테이너는 이름을 가진 인스턴스 변수를 이용해 컨테이너 내부에 있는 각 객체를 설명할 수 없다. 따라서 스몰토크 컨테이너에는 불특정한 수의 이름이 지정되지 않은 인스턴스 변수가 존재하고, 그러한 변수의 수는 컨테이너로 객체를 추가하거나 그로부터 객체를 제거할 때마다 증가 또는 감소하는 것으로 보인다.)


우리 객체의 내부를 확대시켜 인스턴스 변수를 (그리고 그것이 암시하는 "알고 있는," "유지하는," 그리고 "포함하는" 관계들을) 강조할 경우, 우리 카드는 아래와 같은 객체 배열을 제시한다:

Chapter 07 04.png


CRC 카드의 "협동"에 관해 언급한 바는 거의 없지만 인스턴스 변수가 디자인에 전해주는 협력적 관계를 볼 수 있을 것이다.


이번 절에서는 실제 대규모의 문제에서 예상하는 것보단 좀 더 정렬된 상태이다. 거대한 문제를 작업하는 숙련된 디자이너들은 카드에 노트를 갈겨쓰는 것만큼이나 빠르게 카드를 움직일 것이다; 정신없이 써내려가는 중간 중간은 심사숙고와 논의를 위한 휴지기가 될 것이다. 이러한 수고로 만들어진 산물은 좀 더 사려 깊고 명확한 단어로 다시 작성되어 그 자리에 없었던 개발자들에게도 아이디어를 전달할 수 있어야 한다.


일반적인 질문과 답

위의 문제를 시도한 모든 사람이 정확히 동일한 경로를 통해 정확히 동일한 결론에 도달할 가능성은 거의 없다. 흔히 발생하는 차이는 다음과 같다:

  1. Accounts나 Transactions와 같이 클래스명에 복수를 사용하지 않는 이유는? 반박할 수 없도록 잛게 답하자면, 객체 지향 디자이너들에겐 클래스에 단수명을 사용하는 것이 규칙이기 때문이다. 미묘하지만 중요한 다른 답을 한 가지 더 들자면, 복수명을 사용 시 컬렉션을 나타내므로 다른 사람이 클래스를 컬렉션으로 생각하도록 잘못 인도하기는 원하지 않기 때문이다. 클래스는 팩토리로 생각하는 방식이 선호된다 (제 2장).
  2. Account(Checking, Savings, …)와 Transaction(Check, Deposit, …)을 서브클래싱하지 않는 이유는? 이러한 확장(extensions)은 디자인을 좀 더 현실적으로 만들 것이다. 그럼에도 불구하고 먼저 디자인의 가장 단순한 형태를 숙달하고 꾸밈(embellishment)은 후에 숙달하는 편이 낫다. 디자인을 너무 초기에 발전시키는 경우 발생할 수 있는 한 가지 위험은 디자인이 너무 풍부해서 개발자들이 절대로 구현하지 못한다는 점을 들 수 있다. 이는 실패한 프로젝트에서 자주 꼽히는 원인이다. 아무 것도 구현하지 못하는 것보단 간단한 것부터 성취하는 편이 낫지 않은가. (우리 해결책은 너무나 간단해서 deposits를 checks로부터 구별하기 위한 매커니즘으로 양수의 거래액을 이용해 예금(deposit)을 표현하고 음수의 거래액을 이용해 수표나 인출액을 표현하였다. 한 번에 코드를 작성할 수 있을 만큼 간단하다.)
  3. Transactions가 "스스로 처리하는" 책임을 가지도록 만들지 않는 이유는? 수용 가능한 대안적 디자인으로, 컴퓨터 전문가들이 "작업 단위(unit of work)"라고 정의하는 "transaction" 용어와 일관된다. 반면, 이러한 애플리케이션 유형의 일반 사용자들은 check와 같은 일반적인 transaction는 상대적으로 비활성적이고 accounts가 활동의 중심이라고 느낀다. 필자는 전문가의 관점보단 일반 사용자의 관점을 따르는 편이다.
  4. CRC 카드에 클래스 책임 또는 메서드를 어디에 기록해야 하는가? 좋은 질문이다. new와 같은 메서드들은 인스턴스가 이해하지 못하기 때문에 인스턴스의 그림에 속하지 않는다. 클래스만이 그와 같은 메서드들을 이해한다. 그림을 그려야 할 만큼 중요하다면 필자는 주로 카드의 상단 우측 모서리에 적어 둔다. 또한 일부 표기에서와 같이 $와 같은 특수 부표를 앞에 붙인다 [Rumbaugh et al. 1991].
  5. 비교의 책임을 transactions로부터 log로 전환할 수 있는가? 있다. 그리고 이것은 매력적인 대안방법이다. 현재 디자인에서는 log가 그것이 transaction 객체를 포함하는지 알지 못한다; 다만 그것이 포함하는 객체들이 <= 메시지에 응답할 것이라고 가정할 뿐이다. Transaction로부터 <=를 제거하면 log가 (정렬된 컬렉션) 그 내용에 대한 더 많은 지식을 갖춰야 한다는 의미를 가진다. log의 sortBlock은 이제 transactions로부터 일자(date)를 검색하고 비교해야 한다. 그러한 sortBlock을 명시하기 위해 아래의 메시지를 전송하라:
    log sortBlock: [:t1 :t2] t1 getDate <= t2 getDate].
    
    log는 transactions로 비교를 위임하지 않고, 대신 그 일자를 스스로 비교한다. 이와 같은 개념을 이용해 transactions를 금액(amounts) 등 다른 기준으로 정렬할 수도 있다. 아래 메시지를 전송하여 sortBlock을 업데이트하기만 하면 된다:
    log sortBlock: [:t1 :t2] t1 getAmount <= t2 getAmount].
    


논평: 분석, 디자인, 그리고 구현

방법론은 주로 소프트웨어 개발을 세 개의 단계로 나눈다: 분석 (애플리케이션의 요구사항을 사용자가 이해할 수 있는 어휘로 완전히 표현), 디자인 (문제를 해결하기 위해 소프트웨어 구조를 결정), 구현 (디자인을 특정 운영 환경이나 컴퓨터 언어로 만듦). 각 단계는 사실상 이전 단계가 "무엇인지"에 대한 "방법"에 해당한다.


불행히도 누군가의 "무엇"이 다른 사람의 "방법"이므로 단계 간 경계는 어쩔 수 없이 모호하다. "account가 log를 갖도록" 만드는 방법을 결정하는 것은 디자인의 행위일까, 구현 행위일까? (앞서 보인 바와 같이 Account 클래스에 log 인스턴스 변수를 정의할 수도 있고, account 객체를 키(key)로 가지고 로그를 값으로 가지는 dictionary에 하나의 엔트리를 추가할 수도 잇다. Dictionary는 우리가 연구한 작은 개인 시스템에선 지나친 것일 수 있지만 계좌 정보-이름, 번호, PIN과 같은-와 거래 내역이 여러 컴퓨터에 상주할 수 있는 분산 컴퓨팅 환경에서는 더 매력적일 수도 있다. 두 가지 대안책을 강조해볼 때 당신은 디자인을 하는 중인가, 아니면 구현을 하는 중인가?)


"account가 log를 갖도록 만드는" 관계에 부여하는 추상화 수준-디자인 또는 분석-조차 결정에 도움이 되지 못한다. 어떤 것도 관계를 두 개의 추상화 수준에서 표시되지 못하도록 방해하지 않는다. 사실 객체 지향 방법론에서 객체와 그 관계는 주로 하나의 단계에서 다음 단계로 서서히 지속된다. 이렇게 내재적인 모호함에도 불구하고 분석, 디자인, 구현 단계를 구별하려는 시도는 습관적으로 이루어진다. 따라서 주로 84 페이지 도표에 있는 책임 카드를 디자인 단계로, 85 페이지 도표에 있는 스몰토크 스타일의 메서드명은 구현 단계로 돌리곤 한다.


분석에서 디자인으로, 그리고 구현으로 엄격하게 진행되는 것을 역사적으로 개발의 "폭포수" 모델이라 부르는데, 이는 유행이 지났다. 사용자가 프로토타입을 살펴본 후에야 문제를 완전히 이해할 수 있다. 프로토타입이나 워킹 목업(working mock-up)보다 더 많은 피드백을 도출하거나 더 많은 오해 또는 퍼지 분석을 노출하는 것은 없다. 이러한 이유로 최신 디자인이나 분석 방법들은 거의 대부분 "나선형" 또는 "반복적" 개발을 강조하여, 당신이 후속 단계에서 학습한 내용이 앞 단계의 재작업을 유도함을 명시적으로 인정한다.


CRC 카드의 가장 큰 결함, 즉 실제 컴퓨터 시스템의 제약과 편기(biases)로부터 분리되어 있다는 점은 가장 큰 장점이 되기도 한다. 물론 여러 토론을 유발하기도 하지만 이러한 토론의 흔적은 거의 남아있지 않다. 카드는 시간이 지나면서 마침내 분실되고, 이미 발생한 연관(associations)와 의견들도 함께 사라진다. 물론 프로젝트는 초기 CRC 토론으로부터 가능한 한 많은 가치를 보존해야 할 의무가 있다. 이러한 문서화 문제를 해결하기 위한 우세 학파들은 (1) 분석 또는 디자인을 설명하기 위한 컴퓨터화된 도표와 함께 텍스트를 생성하고, (2) 코드에 포함된 중요한 주석을 작성하거나, (3) 이 둘을 결합한다. 객체 또는 CRC 카드가 존재하기 전부터 완전히 만족스러운 해결책은 존재한 적이 없으므로 각 프로젝트는 적절한 정첵을 설정하고 실행해야 한다.


어떤 정책이든 메서드명과 주석에 많이 의존해야 한다. 이것이 바로 메서드명과 주석이 사실상 프로젝트에 참여하는 모든 사람-분석가, 개발자, 검사자-에게 상관되고 의미가 있는 유일한(클래스명과 함께) 개발 산출물이 되는 이유이다. 모든 사람이 이를 이해할 수 있으며, 누군가 이를 변경하더라도 나머지 사람들이 변경내용을 이해할 수 있다. 따라서 클래스명과 메서드명은 문제의 개념적 모델(144 페이지)을 공유하기 위한 공통 언어를 구성한다. 사람들이 타인을 위해 해석할 필요가 없는 개념적 모델은 성공적인 객체 지향 프로젝트의 부수적 영향이다.


훌륭한 분석과 디자인은 또한 시스템의 동적 행위-즉, 여러 개의 상호작용하는 객체들 간 메시지의 순서-를 취급한다. CRC 카드는 시나리오가 실행되는 동안 서로에게 흔들 수 있으므로 (wave about) 동적 디자인의 가장 기본적인 형태를 발전시킬 수 있지만, 습득한 지식을 문서화하기 위해서는 다시 다른 곳에 의지해야 한다. 이 주제는 제 10장에서 좀 더 자세히 다루겠다.


오리지널 CRC 카드에는 [Beck and Cunningham 1989] 두 개의 텍스트 열이 있었고, 첫 열은 책임을, 두 번째 열은 공동 작업자를 열거한다; 공동 작업자란, 현재 클래스가 책임을 수행하기 위해 의존하는 클래스를 의미한다. 하지만 변이형(variants)도 아주 많다. 이번 장에서 공동 작업자의 중요성을 줄여서 간단하게 만든 것은 전통적인 방식과는 떨어진다. 개발자는 CRC 카드 또는 다른 메서드를 이용하는 일부로 이용하든 추후 디자인 단계에서 이용하든 협력(collaborating) 클래스에 관해 충분히 생각해야 한다. Rebecca Wirfs-Brock의 "책임 위주의 디자인" 방법론은 오리지널 카드 포맷을 따르지만 카드에 슈퍼클래스와 서브클래스도 기록하며, 이는 공동 작업자가 지원하는 책임에 따라 그들을 그룹화함을 제시한다. 그렇지만 그녀의 카드 이용 방식은 물리적인 조작을 강조하지는 않는다. 그녀의 저서는 [Wirfs-Brock et al. 1990] 이번 장에 실린 CRC 예제보다 더 크고 훌륭한 예제를 제시하는 동시 객체 지향 디자인과 분석에 관한 표준 교과서들 중 하나에 해당한다. 그 외에 [Rumbaugh et al. 1991; Booch 1994; Jacobson et al. 1992; Coad and Yourdon 1991]이 있다. 마지막으로 [Petroski 1985]의 주제는 훌륭한 디자인의 필수 디자인으로, 앞에 열거한 저서들만큼이나 중요하다. 그는 공학 실패를 조사하였으나 기본이 되는 원칙은 보편적으로 적용된다.


Notes

  1. 방법론이라고도 알려진다. 방법(method)이 더 세련되고 더 정확한 용어지만 객체 지향 프로그래머들이 이미 메서드(method)라는 용어를 사용하고 있다는 단점이 있다. 혼동을 야기할 위험이 있는 경우를 제외하고 둘을 번갈아 가며 사용할 것이다.
  2. 실제 메서드 선택자를 간결하게 설명하는 것은 순수 CRC 카드의 책임 기반 정신에서 벗어난다. 우리 디자인은 스몰토크식으로 치우치는 경향이 있는 동시 클래스가 “하는 일”과 그것을 “하는 방법” 사이에 차이는 모호하게 한다. 하지만 현재로선 메서드명에 약간만 도움을 주어도 다음 장에 실린 코드를 작성하도록 도와줄 것이다.