SmalltalkObjectsandDesign:Chapter 15

From 흡혈양파의 번역工房
Revision as of 10:25, 22 November 2013 by Onionmixer (talk | contribs) (SOAD 15장 다형성 실습하기 페이지 추가)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
제 15 장 다형성 실습하기

다형성 실습하기

14장의 목표였던 다형성의 이해는 그것을 훌륭하게 적용하는 작업에 비하면 그다지 중요하지 않다. 이번 장에서는 실제 문제에 객체 지향의 다형적 해답을 디자인할 수 있는 기회를 제공한다. 이는 사용자의 액션을 실행취소(undo)하는 문제다. 우선 애플리케이션이 필요하겠다.


디자인 연습 1: 도형 에디터

사용자에게 도형(shape)을 생성, 이동 또는 제거하도록 해주는 애플리케이션이 있다고 가정하자. 도형의 종류는 전체적인 디자인에 그다지 영향을 미치지 않으므로 원과 사각형으로 제한하기로 한다. 이 애플리케이션은 많은 상업용 애플리케이션-그래픽 에디터와 시뮬레이터, 네트워크 관리 툴, CASE 툴, 비디오 게임-군에 기반이 된다.


❏ CRC 카드를 이용해 필수 클래스와 그들의 책임에 관한 브레인스토밍을 한다. 브레인스토밍은 두 명에서 네 명으로 구성된 집단일 때 가장 효과가 높다. 힌트를 주자면:

  • 모델에서 벗어나지 마라. 다시 말해, 애플리케이션의 모양과 느낌, 즉 "뷰"는 걱정하지 말라. 이 연습은 사용자 인터페이스가 자신의 디자인에 영향을 미치지 않도록 자신을 훈련하는 데에 목적이 있다. 꼭 필요한 기반 클래스만 디자인하라-어떤 뷰와도 작동할 클래스말이다.
  • Container 객체들은 일상 세계에서 매우 흔하므로 심지어 스스로를 이와 같이 작은 디자인으로 넣을 가능성이 크다는 점을 잊지 말라.
  • 발견한 클래스의 수가 3개 미만인 경우 애플리케이션을 객체 지향적인 방식으로 설명하기에 충분치 않다. 클래스를 충분히 찾지 못하겠다면 문제가 되는 문(statement)에서 명사와 동사를 검색하는 방법에 의지하라.
  • 너무 많은 클래스를 찾는 것도 바람직하지 못하다. 6개를 초과한다면 디자인의 본질을 흐릴지 모른다.


해결책과 논의

필자는 학생들이 해결책을 작업하면서 일반적으로 시도하는 의견들을 그들이 시도하는 순서대로 소개하고자 한다.


가장 분명한 CRC 카드는 Circle과 Square 객체에 대한 카드이다. Shape란 이름으로 된 추상 슈퍼클래스가 원과 사각형에 대한 공통적 기대를 수집하는 데에 편리할 것이라고 예상하기엔 그다지 대단한 스케치가 아니다. 예를 들어, 원과 사각형이 모두 이동이 가능할 것으로 기대할 경우, 그 책임을 Shape 클래스에 기록할 수 있다. 나아가 도형을 어떻게 이동시킬 것인지를 상상하면서 우리는 심지어 각 shape 인스턴스의 위치를 포착하는 인스턴스 변수를 허용할지도 모른다. 우리의 CRC 카드는 아래와 같은 모습일 것이다:

Chapter 15 01.png


Circle과 Square에 특정적인 인스턴스 변수들은 각각 그들의 카드에 표시된다[1]. 애플리케이션은 도형을 "생성, 이동 또는 제거"해야 하므로 위의 Shape "도넛"으로 "생성"과 "제거" 책임을 추가하고 싶을 것이다. 하지만 아래와 같은 이유로 필자는 두 책임을 모두 추가하지 않길 권한다:


첫째, 객체의 생성은 객체의 책임이 아니다. 아직 존재하지 않는 객체가 스스로를 생성할리는 만무하다. 오히려 목격한 바대로 new 메서드와 같은 것을 이용하는 객체의 클래스에게 생성의 책임이 있다. 생성을 위한 책임을 작성하는 것은 괜찮지만 인스턴스를 나타내는 도넛 상에선 안 된다. 가령 카드의 상단 우측 모서리나 그 반대측과 같이 다른 곳에 작성하라.


둘째, Shape 도넛에 (또는 Circle이나 Square 도넛에) "스스로를 제거하라"라고 쓰는 것은 두 가지 이유로 반대한다. 첫 번째로, 당신은 객체에게 스스로 소멸하도록 은유적으로 요청하는 것이며, 프로그래밍 객체는 실제 객체만큼이나 그런 일을 꺼릴 것이다. 둘째, "스스로를 제거하라"라는 표현은 객체가 어디에서 제거되어야 하는지를 암시한다-그렇다면 어디로부터 제거해야 하는가? 정확히 지적해줄 필요가 있다.


두 번째 힌트에 따르면 우리는 container 객체가 필요하다. 꽃을 제거한다면 꽃병에서 제거한다; 책을 제거한다면 도서관이나 책장에서 제거한다. 우리 도형을 포함하는 컨테이너를 뭐라고 부를 것인가? 일반적이지만 바람직하지 않은 해답이 바로 Window이다. 윈도우는 사용자 인터페이스를 제시하며, 우리 디자인이 사용자 인터페이스 또는 뷰 고려 사항에 의해 편향되는 것은 원치 않을 것이다. 모델 측면을 강조하길 원하므로 좀 더 중립적인 단어가 필요하다. 필자는 ShapeRoom을 권한다; 물론 독자가 더 좋은 이름을 생각해낼 수도 있지만 ShapeRoom만으로도 현재 연습을 진행하기엔 충분하다.


도형을 위한 이러한 컨테이너는 여러 개의 책임을 기록하기에 편리한 장소다: 도형을 보관, 제거, 추가, 심지어 이동까지 한다. 이 클래스에 대한 CRC 카드는 오른쪽에 실린 그림과 같다. 이 클래스는 재능이 있는 컨테이너다; 컨테이너와 같은 일반적인 책임을 지원하지만 이를 넘어서 그 내부에서 도형의 이동이 가능하다. 이는 우리가 도형 에디터- ShapeEditor 라고 부를 수 있겠다-에서 기대하는 것을 실행한다. 이 클래스의 인스턴스-모델-는 사용자 인터페이스로부터-뷰-메시지를 처리할 것이다.


Chapter 15 02.png

contents란 이름의 인스턴스 변수는 ShapeRoom이 어떻게 컨테이너와 같은 의무를 충족할 것인지를 제시함을 주목하라: 이 인스턴스 변수는 OrderedCollection 클래스를 참조해야 하는데, 어쩌면 Set의 인스턴스가 될 수도 있다. 다시 말해, ShapeRoom은 그것의 shape 객체들을 포함하기 위해 일반 스몰토크 컬렉션을 구매한다.


"이동"은 Shape 카드뿐만 아니라 ShapeRoom 카드에서도 나타난다는 사실을 주목하라. ShapeRoom은 특정 도형을 특정 지점으로 이동시키기 위한 메시지를 수신한다. ShapeRoom 내에서 이 함수를 차례로 실행하는 메서드는 메시지를 특정 도형에게 전송하여 특정 지점으로 이동시킨다. 이러한 배치는 특이한 방식이 아니다-ShapeRoom 에 대한 고수준의 이동 메서드는 작업을 완성하기 위해 Shape 상에 있는 저수준의 메서드와 협력한다.


다음으로, 앞장의 주제인 다형성을 잠시 검토해보자. 사용자 인터페이스에 도형을 표시하는 사례를 생각해보자. 이는 주로 여기서 디자인하지 않겠다고 언급한 view 클래스의 책임이다. 그럼에도 불구하고 model 클래스들은 스스로에 대한 정보를 view 클래스에서 이용 가능하게 만들어 협력해야 한다. 이러한 책임을 도형의 "렌더링"이라 부른다. 각 유형의 도형은 자신에게 적절한 방식으로 스스로를 렌더링할 것이다; 따라서 원은 사각형과 다르게 렌더링할 것으로 예상해야 할 것이다. View는 shape가 square 인지 circle 인지 알지도 못하고 신경 쓰지도 않는다; 단지 shape가 스스로 수용 가능한 방식으로 렌더링할 것이라고 믿을 뿐이다.


따라서 Circle과 Square 클래스는 각각 render과 같은 메서드를 가질 것이며, Shape 클래스는 동일한 이름으로 된 subclassResponsibility 메서드를 가질 것이다. Shape>>render는 각 서브클래스가 렌더링을 지원해야 하는 의무를 문서화하지만 그 자체는 어떤 일도 하지 않는다. 애플리케이션이 진화하면서 추가 shape 클래스들이 발생할 것이며, 각각은 고유의 render 메서드를 필요로 할 것이다. 다시 말하자면 shapes는 다형성(polymorphic)이다. 앞장의 예제에서와 같이 클라이언트 코드, 이번 경우로 말하자면 view는 shape의 새로운 클래스를 도입하더라도 영향을 받지 않는다.


디자인 연습 2: undo와 redo

이제 간단한 도형 에디터라는 애플리케이션이 있으니 사용자의 액션을 취소 가능하게 만들 수 있길 원할 것이다. Undo는 대화형 애플리케이션을 이전 상태로 되돌리는 것을 의미한다. 강력한 undo를 지원하는 애플리케이션은 사용자로 하여금 익숙하지 않은 기능과 일시적인 생각의 흐름을 탐구하도록 조장하는데, 상황이 악화되더라도 출발점으로 항상 되돌릴 수 있음을 알기 때문이다. 사용자가 undo한 것을 redo시키는 것도 허용한다면 애플리케이션은 훨씬 더 나아질 것이다. 몇 년 전만 해도 대화형 애플리케이션은 redo는 고사하고 undo를 지원하는 경우도 드물었다. 오늘날엔 오히려 정반대다: 물론 정도는 다르지만 대부분의 새로운 애플리케이션들은 undo를 지원하니 말이다. 하지만 무제한의 undo-redo로 애플리케이션에 길들여지면 undo-redo가 제한된 애플리케이션은 시대착오적이 되겠다.


❏ 사용자가 가장 최근이 실행한 액션을 몇 번까지 undo하도록 향상된 도형 에디터를 디자인하라. 가능하다면 CRC 카드와 소규모의 동료 그룹을 이용해 브레인스토밍을 하라.


  • 훌륭한 객체 지향 해결책은 하나의 undo는 물론이고 다중 undo까지 처리할 것이다. 무제한 undo를 목표로 할 수도 있다. 아직은 redo를 걱정할 단계가 아니니 옆길로 새지 않도록 한다. 무제한 undo 문제를 해결하고 나면 약간의 확장만으로 무제한 redo가 가능하다.
  • 언제나처럼 문제가 시작되면 명사와 동사를 찾아보라. 이 문제에서는 특히 더 많은 정보를 제공할 것이다.
  • 4~7개의 클래스가 해결책을 설명하기에 좋은 범위에 속한다. 이러한 수는 원본 애플리케이션에 따라 좌우된다. 따라서 도형 에디터 연습에 주의를 기울인다.
  • 또 다른 적절한 container 클래스가 필요할 것이다.


해결책과 논의

해결책은 객체 지향의 전형적 방법이다; 대화형 애플리케이션에도 작동한다. 이는 1980년대 초에 Object Pascal 개발자들을 위한 MacApp 프레임워크에서 나타났고 그 이후 확산되었다. 이는 디자인 패턴의 상태를 얻기도 했다 (제 18장).


Chapter 15 03.png

해결책을 발견하는 데 있어 열쇠는 문제가 되는 문에 위치한 "undo...action" 절이다. 여기서 "액션"이란 명사(!)에 해당하고, 명사는 항상 객체의 실행 가능한 지원자다. "undo" 동사는 액션 객체의 유망한 책임과 같이 보인다-액션은 스스로를 undo하는 방법을 알아야 한다. 여기까지 수락하고 나면 처음엔 특이하게 들릴지 모르지만 우측에 실린 그림과 같은 카드를 스케치하는 것 외에는 방법이 없다.


문제는 액션이 꽤 모호한 개념이고 이 카드가 그다지 만족스럽지 않다는 데에 있다. 이때 독창적이고 구체적 문제가 끼어든다. 도형 에디터는 정확히 세 개의 함수를 가졌다: 생성, 이동, 제거. 이들 각각은 사용자가 undo할지도 모르는 액션에 해당한다. 이는 우리에게 세 가지 종류의 실행 취소 가능(undoable) 액션, 다른 말로는 Action의 서브클래스 세 개를 제공한다. 아래는 추상 클래스와 세 개의 구체적 서브클래스에 완벽한 배열이다:


Chapter 15 04.png
*=순수 가상적, subclassResponsibility
infor2 = 스스로를 undo (이동) 하는 데에 필요한 정보


undo하는 것은 슈퍼클래스의 subclassResponsibility-아무 일도 하지 않는다-이지만 3개의 서브클래스들의 구체적 책임으로, 서브클래스 각각은 고유의 방식으로 수행한다. 이들은 의무적으로 수행해야 하는 정보는 무조건 유지할 것이다 (인스턴스 변수의 형태로). 예를 들어, Move 객체가 스스로를 undo하는 유일한 수단은 이동되는 shape 인스턴스에 관해서, 그리고 그것이 어디에서 왔는지를 아는 것과 관련된다. 따라서 위 그림에서 Move 인스턴스 내부에 있는 정보는 최소한 shape와 shape의 본래 위치를 나타내는 인스턴스 변수들로 구성된다. (이러한 인스턴스 변수는 다음 절에서 코드를 작업할 때 찾아보라.)


이러한 액션 객체의 라이프 사이클(life cycle)을 이해하기 위해서는 단순한 시나리오를 상상하라: circle을 이동시키고 다시 이동시킨 후 undo를 두 번 실행한다. 각 이동은 구분된 Move 클래스의 인스턴스를 야기한다. 이 두 개의 인스턴스는 순서대로 저장되어야 하며 역순으로 (reverse order) recall과 undo를 실행한다. 어떤 종류의 컨테이너는 분명히 필요한데, 실행 시 순서가 역전되므로 스택이 매우 적절해 보인다. 따라서 애플리케이션이 원을 이동시키면 그것은 Move 객체를 인스턴스화하고 이를 스택으로 밀어 버린다. 사용자가 undo를 한 번 또는 그 이상을 요청할 경우 애플리케이션은 스택으로부터 Move 객체를 하나씩 꺼낸다. Move 객체들은 이 규칙에 응할 수 있는데, 그들은 circle과 그것의 이전 위치를 알기 때문이다. 디자인을 요약하자면 다음과 같다:

Chapter 15 05.png


한 가지 설명되지 않은 부분이 남아 있다: 이 디자인을 원본 에디터의 디자인에 어떻게 추가할 것인가? 별로 변경된 것은 없다. ShapeRoom 의 "도형 이동" 책임에 대한 구현부는 이제 Move 객체를 인스턴스화하고 그것을 스택으로 밀어야 한다. 비슷하게, ShapeRoom의 "추가"와 "제거" 책임은 각각 Create와 Remove 객체를 인스턴스화한 뒤 스택 위로 밀어야 한다. ShapeRoom은 하나의 추가 책임, "undo"를 얻는다. ShapeRoom은 액션이 무엇이든 스택으로부터 최상단(top)에 있는 액션을 꺼내어 스스로를 undo하도록 요청함으로써 "undo" 책임을 실행한다.


"액션이 무엇이든"이라는 말이 무엇을 의미하는지 주목하라: 이는 또 다른 다형적 디자인인 것이다. Room은 꺼낸 액션이 Create, Move, Remove 중 어떤 것의 인스턴스인지 알지도 못할 뿐더러 신경 쓰지도 않는다. 이 문제와 관련해 room은 스택에 어떤 종류의 액션이 위치하는지도 신경 쓰지 않는다; 그저 스스로를 undo해달라는 요청에 어떻게 응답해야 하는지 알 뿐이다. 후에 애플리케이션을 추가 액션 유형으로 확대해야 한다 하더라도 room은 신경 쓰지 않을 것이다-그 코드는 조금도 변경되지 않을 것이니 말이다. 이는 다형성의 가치를 보여주는 전형적인 예이다.


초보 디자이너들은 주로 "이동(move)"과 같은 동사 같은(verb-like) 대상은 객체로 밝혀진다는 속임에 넘어간 것 같은 느낌을 받곤 한다. 결국 원본 도형 에디터에서 "이동"은 책임에 해당했다-객체보다는 메서드에 더 유사한. 그 분석은 올바르고 유효하게 남아 있다. 단지 현재 문제와 무관할 뿐이다-undo 문제만 에디터 문제에 피상적인 관계를 갖고 있다. 에디터 문제는 도형을 조작하는 것과 관련되므로 우리는 shape 객체들을 디자인하였다. 액션은 문제에서 핵심에 해당하므로 클래스의 주된 후보자(candidate)가 되었다. 문제는 도형에서 액션으로 전환되었으므로 디자이너의 관심도 그에 따라 전환되는 편이 나을 것이란 점이다.


마지막으로 실제 디자인은 때때로 진행되다가 말다가를 반복하고, 깔끔한 결론에 도달하는 경우는 드물다는 사실을 기억하라. 따라서 처음으로 이와 같은 문제를 작업하는 디자이너 팀이라면 몇 가지 막다른 길을 경험하고 또 지워나가면서 CRC 카드도 몇 번씩 버려봐야 한다. 훌륭한 디자인은 실패로부터 탄생한다.


undo 구현하기

❏ OrderedCollection 클래스로부터 구매를 통해 StackBuy이란 이름의 스택 클래스를 빌드하라. 스택이 비어 있다면 pop이 nil 객체를 리턴하도록 만들어라. 필요 시 100 페이지에 실린 스택 디자인을 검토하라. 올바르게 작동한다고 확신할 때까지 StackBuy를 검사하라.


❏ OrderedCollection 클래스로부터 상속을 통해 StackInherit이란 이름의 스택 클래스를 빌드하라. 이상적으로는 StackInherit과 StackBuy가 동일하게 행위해야 한다. 즉, 그들의 내부는 다르지만 사용자 또는 클라이언트는 그 차이를 알아선 안 된다. (그렇지만 102 페이지에 논한 상충관계도 기억해야 한다. 스몰토크에서 몇 가지 행위적 차이는 불가피하기 때문이다.) StackInherit도 검사하라.


❏ 작업하고자 하는 코드는 CwExamples 애플리케이션이 당신의 이미지에 위치하도록 요구한다. IBM은 표준 또는 전문가 제품과 함께 이 애플리케이션을 제공한다. (전문가 버전을 사용 중인데 Application Manager에서 나타나지 않는다면 Applications>Available 메뉴 옵션을 선택한 후 CwExamples를 선택하라. 가장 최근 에디션을 로딩하라.)


http://www.claritycomputing.com/books/sod/files에서 student.aps 파일을 받아라. 이 코드는 작동하는 도형 에디터를 구현하는데, 단 undo 기능은 고장난 상태이다. 아래 단계를 따라 하라:

  • Application Manager에서 시작하라.
  • 어떤 애플리케이션이든 하나 선택하라. 무엇이든 상관 없다; 어떤 선택이든 당신이 필요한 메뉴를 활성화할 것이다.
  • File In 메뉴 옵션을 선택하라.
  • 대화 상자에서 student.aps 파일을 찾아서 선택한 후 OK를 눌러라.


❏ ShapeApp 애플리케이션을 선택하고 더블 클릭하여 그 서브애플리케이션을 표시한 후 ShapesModels, ShapesUndoSupport, ShapesViews 서브애플리케이션을 선택 및 표시하라. 아래의 질문에 답하라:

  • 액션 클래스 내 두 개의 주요 메서드의 이름이 무엇인가? 간단한 코드를 읽고 이것이 작업을 수행할 수 있다고 자신을 안심시켜라.
  • 어떤 클래스와 메서드가 Move 객체를 인스턴스화하는가? Remove 객체는 어떤가? Create 또는 Add 객체는 어떤가?
  • Move가 목표 위치를 나타내는 세 번째 인스턴스 변수를 갖는 이유는?


❏ TextualView와 GraphicalView에서 example 클래스 메서드를 시도하라. 문제가 무엇인가? ShapesUndoSupport 애플리케이션에서 Stack 클래스를 이용해 수정하라.


❏ 자신의 스택, StackBuy와 StackInherit을 검증하고 문제를 수정하라.


요약

이 문제에 대한 대부분의 객체 지향 해답들은 Action 대신 Command라는 이름을 사용한다. 이름이 어떻듯 객체 지향 디자인에서 가장 뛰어난 다형성의 애플리케이션에 속한다.


이러한 해결책은 디자인 패턴으로 인식되는데, Command 라고 부르기도 한다 (217 페이지). 이 패턴에서 주목할 만한 두 가지 특성은 다음과 같다:

1. 작은 크기. 코드의 행 수는 수백 개가 아니라 수십 개로 그친다.

2. 최소 메모리. 애플리케이션은 액션을 undo하는 데에 (또는 redoing) 없어서는 안 되는 정보만 저장한다. 많은 사례에서 이러한 정보는 이동(moving)에서와 같이 몇 개의 단어 저장으로 그친다. 반면, 일부 사람들이 고려하는 비객체 지향적 대안책은-모든 액션에 대해 애플리케이션의 전체 상황을 저장-다수준의 undo를 비실용적으로 렌더링하여 너무 빨리 기계 자원을 소모한다.


이번 장에서는 앞에서 살펴보고 남은 객체 지향의 필수요소들을 완전히 살펴보았다. 이 책의 나머지 부분은 초보자들이 읽기엔 힘들겠지만 숙련된 스몰토크 개발자들의 심상 공간에서 일부에 속한다.


Notes

  1. CRC 카드의 orthodox한 사용은 인스턴스 변수의 수준으로 plunge to하지 않는다. 여기서 사용하는 이유는 구현부를 향하는 방식을 평소보다 조금 더 나타내기 위함이다.