SmalltalkBestPracticePatterns:3.2

From 흡혈양파의 번역工房
Jump to navigation Jump to search
3.2 메서드

메서드

메서드는 스몰토크에서 작업을 완료하는 데 필요하므로 시스템에 중요하다. 뿐만 아니라 메서드는 일을 처리하는 방식을 당신의 의도대로 독자에게 전달하는 방법을 의미하기도 한다. 따라서 이 두 가지를 항상 유념하고 메서드를 작성해야 한다. 메서드는 의도한 바대로 작업해야 할 뿐만 아니라 실행해야 할 작업의 의도 또한 전달해야 한다.


메서드는 당신의 프로그램 함수를 쉽게 소화가 가능한 덩어리로 분해한다. 계산을 조심스럽게 메서드로 나누어 조심스럽게 이름을 선정해야만 클래스 명명(naming)을 제외한 다른 프로그래밍 관련 결정에 비해 당신의 목적을 더 많이 독자에게 전달한다.


메서드는 오버라이딩의 입상(granularity)이다. 잘 팩토링된 슈퍼클래스는 슈퍼클래스 코드 일부를 서브클래스로 복사할 필요 없이 단일 메서드를 오버라이딩함으로써 항시 특수화가 가능하다.


메서드는 공짜로 얻을 수 없다. 코드의 모든 조각과 비트를 관리하는 데에는ㅡ처음 작성부터 명명, 기억, 재발견, 어떻게 들어맞는지 전달하기까지ㅡ시간이 소요된다. 따로 얻게 될 이익이 없다면 관리 비용을 비추어 볼 때 작은 메서드보단 큰 메서드가 낫다.


메서드는 성능에서도 비용이 든다. 각 메서드 호출은 정밀한 컴퓨터 주기를 취한다. 양호한 성능을 보장하기 위한 수법으로는 성능 측정을 위해 메서드를 지렛대로 이용하여 좀 더 효과적으로 조정하는 방법이 있다. 필자의 경험에 따르면 잘 팩토링된 코드와 다수의 작은 메서드가 만날 때 좀 더 정확하고 정밀한 성능 측정을 허용하고 (곳곳에 중복된 작은 코드가 없기 때문에) 조정(tuning)에 지렛대 역할을 제공한다(Caching Instance Variable과 같은 기법을 통해).


결국 프로그램을 메서드로 나누는 목적은 당신의 의도를 독자에게 전달하고, 향후 유연성을 제공하며, 필요 시 효과적인 성능 조정에 준비시키기 위함이다.


omposed Method (구성 메서드)

Intention Revealing Selector로 명명된 메서드를 구현하고 있을 때 (p.49).

  • 프로그램을 메서드로 분해하는 방법은?


프로그램은 컴퓨터에 명령을 내리는 것 이상의 작업을 할 필요가 있으며, 사람들과 통신도 해야 한다. 프로그램을 메서드로 분해하는 방법은 (메서드를 얼마나 크게 할 것인지에 더불어) 가능한 한 명확하게 통신하도록 코드를 개선할 때 내려야하는 가장 중요한 결정 중 하나이다. 이러한 결정은 그에 영향을 미치는 많은 요인들, 그리고 전형적으로는 인간의 시간을 들여 기계 자원을 최적화해왔던 프로그래밍 실무의 역사 때문에 복잡해진다.


메시지엔 시간이 소요된다. 작은 메서드를 많이 생산하는 만큼 더 많은 메시지를 실행할 것이다. 프로그램이 얼마나 빠르게 실행될 것인지만 염려하다보면 코드를 단일 메서드에 배열시킬 것이다. 이러한 급진적인 성능 조정은 막대한 인간 비용을 초래할뿐더러 잘 구조화된 코드를 조정하는 성능의 현실을 무시하여 여러 계산차수(order-of-magnitude) 개선을 야기하곤 한다.


큰 메서드가 최고임을 제시하는 요인은 비단 단순한(simple minded) 성능 조정뿐만이 아니다. 다수의 작은 메서드로 구성된 프로그램에서 제어의 흐름을 따르기는 힘들 수도 있다. 스몰토크 초보 프로그래머들이 "실제" 작업이 어디서 진행되는지 알아낼 수 없다고 불평하는 경우가 종종 있다. 경험이 늘어나면서 여러 객체를 통해 제어의 흐름을 이해해야 하는 경우는 줄어들 것이다. 메시지 이름을 잘 선택하면 호출된 코드의 의미를 올바르게 추측할 수 있다.


의도를 보여주는 메시지 이름을 통한 통신 기회야말로 메서드를 작게 유지해야 한다는 주장의 가장 설득력 있는 이유일 것이다. 사람들이 당신의 프로그램을 더 자세히 이해할 수 있다면 프로그램을 좀 더 빠르고 정확하게 읽어 그 세부 내용을 더 고차원의 구조로 단위화(chunk)할 것이다. 프로그램을 메서드로 분리하면 그러한 단위화를 야기할 기회를 제공한다. 이는 당신이 시스템 구조를 미묘하게 전달하는 방법이다.


메서드가 작으면 관리도 용이하여 추측으로부터 분리시켜준다. 올바른 작은 메서드로 적힌 코드는 소수의 메서드만 변경하여 그 연산(operation)을 수정 또는 개선하도록 요한다. 버그를 수정하거나, 특징을 추가하거나, 성능을 조정 시에도 해당한다.


작은 메서드는 상속을 수월하게 만들기도 한다. 큰 메서드로 작성된 클래스의 행위를 특수화하기로 결정한 경우, 슈퍼클래스로부터 코드를 서브클래스로 복사하여 몇 줄을 변경해야 하는 경우가 종종 있다. 그러면서 슈퍼클래스 메서드와 서브클래스 메서드에 다수의 업데이트 문제가 야기된다. 메서드가 작으면 행위의 오버라이딩은 단일 메서드의 오버라이딩과 같아진다.


  • 하나의 식별 가능한 작업을 실행하는 메서드들로 프로그램을 분해하라. 모든 연산은 추상화(abstraction)와 동일한 수준에 있는 메서드에 유지하라. 이는 자연스럽게 각각이 몇 개의 줄로 이루어진 작은 다수의 메서드로 이루어진 프로그램을 도출할 것이다.


Composed Method를 하향식(top-down)으로 이용할 수 있다. 메서드를 작성하면서 여러 개의 더 작은 메서드를 (아직 구현하지 않고) 호출할 수 있다. Composed Method는 당신의 개발품을 여러 조각으로 분해하는 사고의 도구(thought tool)이다. 여기 하향식 Composed Method의 예를 들어보겠다:

Controller>>controlActivity
	self controlInitialize.
	self controlLoop.
	self controlTerminate


Composed Method를 상향식으로 사용하여 공통 코드를 한 장소에서 팩토링할 수도 있다. 동일한 표현식(expression)을 사용하고 있음을 발견할 경우 (코드 3줄, 2줄 또는 1줄) 표현식을 그것의 메서드에 넣고 필요할 때 호출함으로써 코드를 개선할 수 있다.


아마도 가장 중요한 것은, 구현하는 도중에 새로운 책임을 발견할 때 Composed Method를 사용할 수 있다는 점일 것이다. 한 객체로부터 다른 객체로 2개 또는 이상의 메시지를 단일 메서드로 전송할 때마다 그러한 메시지를 결합하는 수신기(receiver)에서 Composed Method를 생성할 수 있을 것이다. 그러한 메서드들은 당신의 시스템 중 다른 부분에서도 변함없이 유용하다.


Constructor Method로 객체를 생성하라 (p.23). 부울(boolean) 표현식을 Query Method에 넣어라 (p.30). 다른 곳에서 작업을 원하면 메시지를 호출하되 가끔은 위임을 사용하라 (p.64). 임시 변수(p.103)는 임시 저장에 사용하라. Constant Method로 상수를 나타내라 (p.87).


Constructor Method (생성자 메서드)

Composed Method(p.21)가 객체를 생성하였다.

  • 인스턴스 생성을 어떻게 표현하는가?


인스턴스 생성을 나타내는 가장 유연한 방법은 간단하게 "새로운" 메서드 다음에 클라이언트로부터 새 인스턴스로 향하는 일련의 메시지가 따라오는 방법이다. 의미가 유효한(valid) 다른 파라미터(parameter) 조합이 있다면, 고객은 자신이 원하는 파라미터의 활용할 수 있다.


이 스타일에서 Point를 생성하면 다음과 같은 모습이다:

Point new x: 0; y: 0


이 접근법에서는 한 곳에서 객체를 구성하다가 다른 객체로 전달하여 구성을 완료시킨다는 점에서 추가로 유연성이 제공된다. 모든 생성 파라미터를 한 장소에 놓기 위해 디자인을 수정하지 않아도 된다면 이 방법은 통신을 단순화시킬 수 있다.


반면, 당신이 원하는 것을 결정했다고 치면 클래스에 대해 가장 먼저 무엇이 알고 싶은가? 첫 번째 질문은 "인스턴스를 생성하기 위해선 무엇이 요구되는가?"가 되겠다. 클래스 제공자로서 당신은 이 질문에 대해 가능한 한 간단한 답을 원할 것이다. 위에 설명한 스타일의 경우, 사용 가능한 인스턴스를 생성하는 방법을 알아내기 이전에 우선 클래스로의 참조를 찾아낸 후 코드를 읽어야 할 것이다. 코드가 복잡하다면 인스턴스를 생성하는 데에 무엇이 필수적이고 무엇이 선택적인지 알아내기까지 시간이 어느 정도 소요될 것이다.


아니면 각 유효한 인스턴스 생성 방법을 표현하는 메서드가 존재하도록 확보하는 방법도 있다. 이 방법을 택한다면 인스턴스 생성 메서드가 급증하는 결과를 낳게 될까? 그럴 가능성은 거의 없다. 대부분 클래스에는 인스턴스를 생성하는 방법이 하나밖에 없다. 이를 제외한 예외의 경우라 하더라도 인스턴스 생성 방법의 수는 손에 꼽는다. 올바른 파라미터 조합의 수가 수십만 개에 달하는 극히 드문 사례들의 경우, 공통 사례(common case)를 위해 Constructor Method를 사용하고 나머지는 Accessor Method를 제공하라.


이러한 스타일의 인스턴스 생성의 경우, "어떻게 유효한 인스턴스를 생성할 수 있을까?" 라는 질문에 대해 클래스 메서드의 "instance creation" 프로토콜을 살펴보면 쉽게 답할 수 있을 것이다. Intention Revealing Selectors(의도를 알려주는 선택자)는 인스턴스가 하는 일을 알려주는 반면 Type Suggesting Parameter Names(타입 자세 파라미터명)는 필요한 파라미터를 전달한다.


  • well-informed 인스턴스를 생성하는 메서드를 제공하라. 필요한 파라미터를 모두 그러한 메서드들로 전달하라.


Point class>>x:y: 는 필요로 하는 두 가지 수를 모두 파라미터로 취하므로 Constructor Method이다.


어떤 사람들은 인스턴스를 생성하는 동시에 초기화될 수 있도록 Constructor Method에서 키워드를 인스턴스 변수와 동일한 이름으로 지정해야 한다고 생각한다. 당신은 선택자(Intention Revealing Selector)를 이용해 더 많은 의도를 표현할 방법을 항시 모색해야 한다. 예를 들어, Point class>>r:theta: 는 필자가 극좌표에서 작업 시 추가하는 Constructor Method이다:

Point class>>r: radiusNumber theta: thetaNumber
      ^self
	 x: radiusNumber * thetaNumber cos
      	 y: radiusNumber * thetaNumber sin


SortedCollection class>>sortBlock: aBlock은 사용할 준비가 된 SortedCollection을 리턴하기 때문에 Constructor Method이다. SortedCollection class>>new 또한 사용 준비가 된 SortedCollection을 리턴하므로 Constructor Method이다. 다만 기본(default) 정렬 블록을 포함할 뿐이다.


Constructor Method를 "instance creation(인스턴스 생성)"이라 불리는 메서드 프로토콜로 넣어라.


메서드가 파라미터를 취할 경우 Constructor Parameter Method가 필요할 것이다(p.25). 파라미터 타입이 아니라 파라미터 기능을 설명하는 Intention Revealing Selector를 (p.49) 당신의 메서드에 제공하라. 광범위하게 사용되는 Constructor Method는 Shortcut Constructor Method를 (p.26) 사용해도 좋다.


Constructor Parameter Method (구성자 파라미터 메서드)

Constructor Method(p.23)는 새 인스턴스 상으로 파라미터를 전달할 필요가 있다. 당신은 공통 상태(p.80)를 초기화해야 한다.

  • 파라미터로부터의 인스턴스 변수를 Constructor Method로 설정하는 방법은?


Constructor Method의 파라미터를 클래스로 넘겼다면 이것을 어떻게 새로 생성된 인스턴스로 넘길까?


가장 유연하고 일관된 방법은 Setting Methods 를 이용해 모든 변수들을 설정하는 방법이다. 따라서 2개의 메시지로 Point 가 초기화될 것이다:

Point class>>x: xNumber y: yNumber
      ^self new
      	    x: xNumber;
	    y: yNumber;
	    yourself


이 접근법을 사용 시 직면했던 문제는 Setting Methods 가 복잡해진다는 점이다. 초기화 도중에 전송이 되는지 확인하기 위해 특수 로직을 Setting Methods 로 추가해야 했다; 전송이 되고 있다면 변수만 설정하면 될 일이다.


앞에서 설명한 "꼭 한 번만" 이라는 규칙을 기억하는가? 초기화 도중에 사용을 위해 Setting Method를 특수 케이싱(special casing)하는 것은 첫 번째 규칙에 위반된다. 당신에겐 두 가지 상황이 있지만-인스턴스 생성 도중에 상태 초기화와 계산 도중에 상태 변경-메서드는 하나만 존재한다. 알려야 하는 사항이 두 가진데 하나만 알려주는 격이다.


Setting Method 해결방법은 만일 모든 변수의 타입을 보길 원할 경우 여러 개의 메서드에서 Type Suggesting Parameter Names를 살펴봐야 하는 단점도 있다. 아마도 당신은 독자들이 코드를 보고 재빨리 인스턴스 변수의 타입을 이해할 수 있길 바랄 것이다.


  • 모든 변수를 설정하는 단일 메서드를 코딩(code)하라. 그리고 "set"를 다음에 변수의 이름을 붙여라.


이 패턴을 사용하면 위의 코드는 아래와 같아진다:

Point class>>x: xNumber y: yNumber
      ^self new
      	    setX: xNumber
	    y: yNumber
Point>>setX: xNumber y: yNumber
      x := xNumber.
      y := yNumber.
      ^self


setX:y:에서 Interesting Return Value(흥미로운 리턴값)을 주목하라. 그 곳에 위치하는 이유는 메서드의 리턴 값이 호출자(caller)의 리턴 값으로서 사용될 것이기 때문이다.


Constructor Parameters Methods를 "private"이라 불리는 메서드 프로토콜에 넣어라.


Explicit Initialization(p.83)을 사용 중이라면 지금 그것을 호출하여 초기화가 인스턴스 생성의 일부임을 전달하기 좋은 시기다.


Shortcut Constructor Method (간단한 생성자 메서드)

전반적인 Constructor Method를 확인하였다(p.23).

  • Constructor Method가 너무 길 때 새 객체를 생성하기 위한 외부 인터페이스는 무엇인가?


새 객체를 생성하는 일반적인 방법은 새 인스턴스를 생성해주는 클래스로 메시지를 전송하는 것이다; "Point x: width y: height". 이 방법이 바람직한 이유는 어떤 객체가 생성되는지가 매우 명시적이기 때문이다. 이 표현식의 결과로 발생하는 결과를 알아내려면 어딜 살펴봐야 할지 알고 있을 것이다.


객체 생성을 위한 이러한 인터페이스 스타일에 두 가지 문제가 있는데, 길이가 길다는 점이 가장 중요한 문제겠다. Point class>>x:y: 가 point의 생성을 위한 유일한 인터페이스라면 스몰토크 소스 파일은 몇 퍼센트씩 증가할 것이다. 매우 공통적으로 사용되는 객체들의 경우, 되돌려 더 긴 형태로 전송하는 argument 중 하나로 메시지를 전송함으로써 좀 더결한 인터페이스를 생성할 수 있다.


객체 생성을 위한 명시적 클래스 기반 인터페이스에서 두 번째 문제는 오해를 불러일으킬 수 있다는 점이다. argument의 클래스 내 차이가 메시지에 의해 리턴되는 구체적 클래스를 변경하는 경우가 있다. 예를 들어, Collections 종류에 따라 다른 유형의 Streams를 필요로 하는 때가 있다.


객체 생성을 메시지로서 다른 argument에게 표현 시 너무 간결하다는 점 또한 단점이다. 그러한 메시지는 언어 구문에서 빌드 시 쉽게 오해를 부른다 (예: "@" 가 전형적인 예이다.). 이는 프로그래머가 메시지를 기억해야 하는 부담을 준다. 또한 클래스의 인스턴스 생성 메서드를 살펴볼 때 쉽게 찾을 수가 없다. 하지만 생성자 메서드(constructor method)에 대한 명료성과 정밀성의 이익은 실로 상당하다.


  • Constructor Method에 대한 argument 중 하나를 향해 객체 생성을 메시지로 표현하라. 개발 중인 시스템마다 Shortcut Construct 메서드를 추가할 때는 그 수를 3개 이하로 하라.


스몰토크에서 전형적인 예제로 Point 생성을 들 수 있다. Constructor Method는 다음과 같다:

Point class>>x: xNumber y: yNumber
      ^self new
      	    setX: xNumber
	    y: yNumber


Shortcut Constructor Method는 다음과 같다:

Number>>@ aNumber
      ^Point
	    x: self
	    y: aNumber


재미있게도 ParcPlace 이미지는 Point>>extent: 와 Point>>corner:를 Rectangles에 대한 Shortcut Constructor Method로서 사용으로부터 멀어지고 있다.

Shortcut Constructor Method를 "converting"이라 불리는 메서드 프로토콜에 넣어라.


변환(Conversion)

  • 어떻게 한 객체의 포맷에서 다른 객체의 포맷으로 정보를 변환하는가?


고객마다 같은 정보를 다른 프로토콜로 표현하길 원하는 경우가 있다. 예를 들어, 어떤 객체는 정렬된 Collection을 살펴보아야 하고, 중복된 내용(duplicate)이 제거된 상태를 원하기도 한다.


가장 간단한 해결방법은 프로토콜을 요구하는 각 객체마다 필요한 프로토콜을 모두 추가하는 방법이다. 이는 불필요하게 큰 public 프로토콜을 야기하여 publication과 이해가 힘들게 된다. 동일한 선택자가 고객마다 다르게 의도해야 하는 경우도 있는데, 그럴 경우 이 접근법은 실행이 불가해진다.


  • 한 객체의 프로토콜만 덮어씌우기보다 객체를 다른 객체로 변환하라.


일부 변환은 유사한 객체에서 발생하는데, 8 비트 ASCII 문자의 문자열(String)을 16 비트 ISO 문자의 문자열로 변경하는 것을 예로 들 수 있겠다. 일부 변환은 상이한 객체에서 발생하며, String을 Data로, 또는 Number를 Pointer로 변환하는 것이 예가 된다.


비슷한 의무를 가진 객체를 리턴하는 변환은 Converter Method를 (p.28) 사용해야 한다. 다른 프로토콜로 된 객체로 변환하고자 하는 경우 Converter Constructor Method를 (p.29) 사용하라.


Converter Method (컨버터 메서드)

현재 Conversion을 구현하고 있다.

  • 하나의 객체를 프로토콜은 같지만 포맷이 다른 객체로 단순 변환시키려면 어떻게 표현하는가?


오랜 시간동안 String>>asDate 메서드가 있다는 사실이 필자를 괴롭혔는데, 그 존재에서 무엇이 그토록 신경이 쓰였는지 분명히 지적해내기가 힘들었다. 따라서 프로젝트에서 변환이라는 아이디어를 극단으로 몰고 곳을 살펴보았다. 모든 도메인 객체에는 20개 또는 30개의 변환 메서드가 있었다. 새 객체가 추가될 때마다 나머지 시스템과 작업을 시작하기도 전에 20개 또는 30개의 메서드를 가져야 했다.


변환할 객체에 변환을 메서드로서 표현하는 데 있어 한 가지 발생하는 문제는 추가 가능한 메서드의 수가 제한되지 않았다는 점이다. 따라서 프로토콜이 제한이 없이 계속 증가한다. 또 다른 문제는, 의식하지 못할만한 클래스와 수신자를 매우 미약하게 연결시킨다는 점이다.


필자는 아래와 같은 경우 메시지와 함께 변환을 객체로 표현함으로써 프로토콜 폭발(protocol explosion)을 피한다:

  • 변환의 소스와 목적지(destination)가 동일한 프로토콜을 공유할 때.
  • 변환을 구현하는 합리적인 방법이 하나만 존재할 때.


  • 변환될 객체에 새 객체로 변환시키는 메서드를 제공하라. 메서드 이름은 리턴되는 객체의 클래스 앞에 "as" 를 붙여 명명하라.


아래 몇 가지 예제를 들어보겠다. 리턴된 객체가 수신자와 동일한 프로토콜을 가진다는 사실을 주목하라 (Sets는 Collections처럼, Floats는 Numbers처럼 행동한다).

Collection>>asSet
Number>>asFloat


Converter 메서드는 "private"이라 불리는 메서드 프로토콜에 넣어라.


변환에 Intention Revealing Selector를 (p.49) 선택하라.


Converter Constructor Method (컨버터 생성자 메서드)

새로운 유형의 객체로 Conversion을 (p.28) 구현할 필요가 있다.

  • 한 객체가 프로토콜이 다른 객체로 변환되는 것을 어떻게 표현하는가?


많은 면에서 볼 때 변환의 존재를 가장 간단하게 전달하는 방법은 Converter Method를 이용하는 방법이다. 당신에게 Date를 설명하는데 이미 Strings를 알고 있는 상태라면 "asDate를 String으로 전송함으로써 String을 Date로 변환하면 되잖아요,"라고 말할 것이 분명하다.


이 해결방법은 본래 목적과 무관한 프로토콜을 가진 Numbers나 Strings와 같은 변환에 대한 공통적 소스의 클러터링(cluttering)을 위험에 빠뜨린다. String에 대한 비주얼 스몰토크 구현에는 36개의 메서드가 있으며, 그 중 절반은 완전히 다른 프로토콜로 된 객체를 리턴한다. 필자는 String에 100개 이상의 Conversion Method로 "향상된" 애플리케이션들을 봐왔다.


  • 변환될 객체를 argument로 취하는 Constructor Method를 만들어라.


예를 들어, Date class>>fromString: 은 Converter Constructor Method이다. 이는 변환될 String를 argument로서 취하고 Date를 리턴한다.


Converter Constructor Method를 "instance creation(인스턴스 생성)"이라 불리는 프로토콜에 넣어라.


메서드에 대해 Intention Revealing Selector를 (p.49) 선택할 필요가 있다.


Query Method (쿼리 메서드)

Composed Method(p.21)가 부울식 표현식을 실행했다.

  • 객체의 속성 시험을 표현하는 방법은?


사실 여기서 두 가지 결정을 내려야 한다. 첫 번째는 속성을 시험하는 메서드로부터 무엇을 리턴하는지와 관련된 결정이다. 두 번째는 메서드의 이름을 무엇이라고 정할 것인지에 관련된다.


Query Method에 대한 프로토콜을 설계하면 두 가지 대안책을 제공한다. 첫 번째는 두 객체 중 하나를 리턴하는 것이다. 예를 들어, 켜거나 끌 수 있는 스위치가 있다면 #on 또는 #off를 리턴할 수 있겠다.

Switch>>makeOn
	Status := #on
Switch>>makeOff
	status := #off
Switch>>status
	^status


그러면 고객은 Switch가 그 상태를 어떻게 저장하는지 알기만 하면 된다.

WallPlate>>update
	self switch status = #on ifTrue: [self light makeOn].
	self switch status = #off ifTrue: [self light makeOff]


보수 프로그래머가 천진난만하게 Symbols를 #On와 #Off로 변경하기로 결정한다면 고객의 뜻을 어길 것이다.


메시지만 기반으로 한 관계를 유지하는 편이 훨씬 더 쉽다. Symbol을 리턴하는 상태보다는 Switch가 부울값(Boolean)을 리턴하는 단일 메서드를 제공하는 편이 낫다; Switch가 켜져 있으면 true, 꺼져 있다면 false.


Switch에서 부울값을 포함하는 변수로서 표현되든, 아니면 2개의 Symbol 중 하나를 포함하는 변수로서 표현되든, 이것은 프로토콜의 설계와는 무관하다.


단, 명명과 관련된 질문은 약간 까다롭다. 속성을 시험하고 부울값을 리턴하는 메서드의 가장 간단한 이름은 그냥 간단하게 지으면 된다. 위의 예제에서 필자라면 메서드를 "on" 라고 부르고 싶을 것이다:


하지만 이는 혼란을 야기한다. "on" 이 과연 "켜져 있습니까?" 라는 의미일까, 아니면 "켜시겠습니까?" 라는 의미일까.


  • 부울값을 리턴하는 메서드를 제공하라. 속성 이름 앞에 "be"-is was, will 등-를 붙여 명명하라.


스몰토크의 예를 몇 가지 들어보겠다:

isNil
isControlWanted
isEmpty

Query Method의 logical inverse를 많이 사용할 경우 notNil 또는 notEmpty와 같은 inverse 메서드도 함께 제공하라. 사실 inverse는 긍정적으로 나타낼 수 있는 방법을 찾을 수 있다면 금상첨화겠다. 하지만 isUseful 또는 isFull은 앞뒤에 맞지 않다.

Query Method를 "testing(시험 중)" 이라 불리는 프로토콜에 넣어라.


Comparing Method (비교 메서드)

  • 객체들을 어떻게 서로 관련시켜 정렬할까?


비교 메시지, <, <=, >, >= 는 대부분 Magnitude와 그 서브클래스에서 구현된다. 이는 주로 모든 유형의 목적에 사용된다-정렬, 필터링, threshold 검사.


새로운 객체를 생성할 때는 스스로 비교 메서드를 구현할 수 있는 선택권을 가진다. 필자가 스몰토크를 사용한지 1~2년차였을까, 객체 유형을 SortedCollection에 넣을 때마다 비교 메서드를 구현했던 기억이 난다. 시간이 흐르면서 정렬 블록을 더 사용하게 되었고 (SortedCollection) "<=" 의 구현 빈도는 줄었다.


그렇다 하더라도 새 객체를 정렬하는 방법이 하나밖에 없을 때 여전히 "<=" 를 구현한다. 그러면 이것을 사용하는 대상들은 그러한 객체들을 포함한 집합체(collection)를 가져와 "asSortedCollection" 라고 말하여 정렬한다.


사용자 인터페이스에서 정렬을 사용하면 대부분 단일 비교 정렬 시 제공되는 것보다 더 많은 유연성을 요한다. Temporarily Sorted Collection(임시 정렬 집합체)를 이용한 정렬 블록의 사용을 기대하라


  • argument 전에 수신자에게 명령해야 하는 경우 "<=" 를 구현하여 true를 리턴하라.


Numbers야말로 Comparing Method를 분명하게 보여주는 예제이다. Characters와 Strings 또한 Comparing Method를 구현한다.


대기(timed) Events에 대한 Collection이 있다면 Comparing Method를 이용해 시간별로 정렬할 수 있다:

Event>><= anEvent
	^self timestamp <= anEvent timestamp


Comparing 메서드를 "comparing(비교 중)" 이라 불리는 프로토콜에 넣어라.


정렬은 종종 다른 객체들의 정렬에 대한 Simple Delegation(p.65)와 관련해 이루어진다. 다중 정렬(multiple orderings)의 대해서는 Temporarily Sorted Collection(p.155)를 사용하라.


Reversing Method (역 메서드)

메시지가 너무 많은 수신자로 향하여 Composed Method(p.21)가 올바로 읽히지 않을 수 있다. 여러 다른 객체가 메시지를 수신해야 하기 때문에 Cascade(p.183)의 모습이 올바르지 않아 보일 수도 있다.

  • 어떻게 부드러운 메시지 흐름을 코딩하는가?


훌륭한 코드는 이해하기 쉬운 리듬을 가진다. 리듬을 깨는 코드는 읽거나 이해가 힘들다.

Point>>printOn: aStream
	x printOn: aStream.
	aStream nextPutAll: ' @ '.
	y printOn: aStream


여기 메시지가 3개의 다른 객체로 향하고 있다고 치자. 이를 세 부분(three part) 연산으로 읽고 싶은데 연산이 세 가지 다른 객체에서 존재하므로 한데 모아 두기가 힘들다.


우리는 모든 메시지가 하나의 객체를 거치도록 확보하여 문제를 해결할 수 있다. 하지만 재미삼아 새로운 선택자를 생성하는 것은 좋지 못한 생각이다. 시스템 내 각 선택자는 실제적 문제를 해결함으로써 자신의 존재를 정당화해야 한다; 중요한 결정을 부호화하는 등.


코드를 좀 더 유연하게 만들기 위해 새로운 선택자의 새로운 메서드를 추가하는 것이 선택자 명칭 공간을 유용하게 활용하는 방법이다.


  • 파라미터 상에서 메서드를 코딩하라. 그 이름은 원본 메시지로부터 도출한다. 본래 수신자를 새 메서드에 대한 파라미터로 취하라. 본래 수신자에게 원본 메시지를 전송함으로써 메서드를 구현하라.


Stream>>print:를 정의함으로써 위의 메서드를 깔끔하게 다듬을 수 있다:

Stream>>print: anObject
	anObject printOn: self
Point>>printOn: aStream
	aStream
		print: x;
		nextPutAll: ' @ ';
		print: y


이 패턴은 위태로울 정도로 순수한 미에 가까워 보인다. 하지만 이를 사용하고 싶다는 욕구를 느끼고 나면 꼭 사용해야겠다는 절대적 필요성이 뒤따른다. 모든 메시지가 하나의 객체로 향하도록 확보하고 나면 그 객체는 어떠한 파라미터에도 영향을 미치지 않고 쉽게 변화가 가능하다.


역행(reversed)하는 메시지의 이름을 딴 메서드 프로토콜에 Reversing Methods를 넣어라. 예를 들어, Stream>>print: 는 "printing(인쇄 중)" 이라는 메서드 프로토콜에 위치한다.


메서드 객체(Method Object)

Composed Method(p.21)로 잘 간소화되지 않은 메서드가 있다.

  • 코드의 여러 행이 다수의 argument와 임시 변수를 공유하는 경우 어떻게 메서드를 코딩하는가?


보통 복합 시스템 중심에서의 행위는 복잡하다. 처음엔 복잡성이 인지되지 않는 것이 일반적이어서 행위는 단일 메서드로서 표현된다. 점차 메서드가 증가할수록 행의 수가 증가하고, 파라미터 수, 임시 변수의 수가 거대해질 때까지 증가한다.


그러한 메서드에 Composed Method를 적용시키면 통신을 향상시키기는커녕, 상황을 까다롭게 만든다. 그러한 메서드의 모든 부분들은 모든 임시 변수와 파라미터를 필요로 하므로 당신이 분리시키는 메서드의 어떤 조각이든 6개 또는 8개 파라미터를 필요로 한다.


이에 대한 해결책으로는, 객체가 메서드 호출을 표현하도록 생성하고, Composed Method를 이용해 간략화가 가능하도록 객체 내에서 인스턴스 변수의 공유 명칭 공간을 사용하도록 만드는 방법이 있다. 하지만 이러한 객체들은 대부분의 객체들과는 매우 다른 특징을 가진다. 대부분 객체들은 명사인데 반해 앞에서 언급한 객체들은 동사이다. 일반 객체들은 고객에게 쉽게 설명이 가능하지만 이 객체들은 실생활에 유사한 형태가 없어 설명이 쉽지 않다. 하지만 메서드 객체들은 그 특이한 특성만큼 가치가 있다. 시스템의 행위에 대해 중요한 부분을 표현하기 때문에 아키텍처 중심이 되곤 한다.


  • 메서드의 이름을 딴 클래스를 생성하라. 여기에 본래 메서드의 수신자에 대한 인스턴스 변수, 각 argument, 각 임시 변수를 제공하라. 본래 수신자와 메서드 argument를 취하는 Constructor Method를 제공하라. 그리고 본래 메서드의 body를 복사하여 구현되는 인스턴스 메서드, #compute를 하나 부여하라. 이 메서드 대신 새로운 객체의 인스턴스를 생성시키고 #compute를 전송하는 메서드로 교체하라.


이 패턴은 필자가 책에서 마지막으로 추가한 패턴이다. 사용하는 경우가 극히 드물어서 포함시키지 않으려 했다. 그런데 이 패턴을 이용해 큰 계약을 성사시키도록 주요 고객을 설득시킬 수 있었다. 그래서 이 패턴이 필요할 경우에는 정말로 애타게 필요로 한다는 사실을 깨달았다.


코드는 아래와 같은 모습이다:

Obligation>>sendTask: aTask job: aJob
	| notProcessed processed copied executed |
	...150 lines of heavily commented code...


먼저, Composed Method를 시도했다. 메서드의 한 조각을 잘라낼 때마다 파라미터와 4개의 임시 변수로 전송해야 한다는 사실을 깨달았다:

Obligation>>prepareTask: aTask job: aJob notProcessed: notProcessedCollection processed: processedCollection copied: copiedCollection executed: executedCollection


모양도 못생겼을 뿐더러 최종 호출에서 코드의 행이 전혀 줄어들지 않았다 (Indented Control Flow를 참고). 15분쯤 고민하다 본래 메서드로 가서 메서드 객체를 사용했다. 먼저 아래와 같은 클래스를 생성했다:

Class: TaskSender
      superclass: Object
      instance variables: obligation task job notProcessed
processed copied executed


클래스의 이름은 본래 메서드의 선택자로부터 직접 취한 것을 주목하라. 그리고 본래 수신자, 2개의 argument, 4개의 임시 변수 모두 인스턴스 변수가 된 것을 주목하라.


Constructor Method는 본래 수신자와 2개의 argument를 파라미터로 취했다:

TaskSender class>>obligation: anObligation task: aTask
job: aJob
	^self new
	      setObligation: anObligation
	      task: aTask
	      job: aJob


다음으로 본래 메서드에서 코드를 복사했다. "aTask" 를 "task" 로, "aJob" 을 "job" 으로 문자만 변경했는데, 파라미터는 인스턴스 변수와 다르게 명명되기 때문이다. 아, 참! 그리고 임시 변수는 이제 인스턴스 변수이므로 그 선언부를 삭제하였다.

TaskSender>>compute
	...150 lines of heavily commented code...


그리고 난 후 본래 메서드가 TaskSender를 생성 및 호출하도록 변경하였다:

Obligation>>sendTask: aTask job: aJob
	(TaskSender
		obligation: self
		task: aTask
		job: aJob) compute


잘못 손댄 것이 없는지 확인하기 위해 메서드를 시험적으로 사용해봤다. 텍스트만 매우 조심스럽게 이동한 상태였기 때문에 수정된 메서드와 그에 관련된 객체는 처음엔 잘 작동했다.


여기서 흥미로워진다. 이제 메서드의 모든 조각들은 동일한 인스턴스 변수들을 공유하기 때문에 어떤 파라미터도 전달하지 않고 Composed Method를 사용할 수 있었다. 예를 들어, Task를 준비했던 코드 조각이 이제 #prepareTask라 불리는 메서드가 된 것이다.


여기까지 작업을 진행하는 데에 2시간 정도가 소요되었지만 작업을 완료했을 때 #compute 메서드는 마치 문서와 같이 읽혔다; 인스턴스 변수 3개를 삭제했고, 전체로 볼 때 코드 길이는 원본에 비해 절반으로 줄었으며, 본래 코드에 있던 버그 또한 발견 및 수정할 수 있었다.


Execute Around Method (주변 실행 메서드)

  • 함께 실행해야 하는 액션들을 어떻게 표현하는가?


하나의 객체로 전송되는 메시지가 동시에 호출되는 경우는 흔하다. 파일은 한 번 열리면 언젠간 닫혀야 한다. 그리고 컨텍스트를 누르면 창이 떠야 한다.


이를 표현하는 분명한 방법은 두 메서드를 객체의 외부 프로토콜의 일부로서 publish 하는 방법이다. 고객은 올바른 순서로 이 둘을 명시적으로 호출할 필요가 있으며, 앞의 것이 호출되면 뒤의 것도 확실히 호출되도록 확보해야 한다. 이로 인해 객체의 학습이나 사용이 더 힘들어지고, 파일 기술자 누출(file descriptor leaks)와 같은 결함을 야기한다.


  • Block을 argument로 취하는 메서드를 코딩하라. 처음 호출해야 하는 메서드 이름 앞에 "During: aBlock" 를 붙여 메서드를 명명하라. Execute Around 메서드의 body에서 첫 번째 메서드를 호출하고, 블록을 평가한 후 두 번째 메서드를 호출하라.


필자는 Cursor>>showWhile:에서 이 패턴을 학습하였다:

Cursor>>showWhile: aBlock
	| old |
	old := Cursor currentCursor.
	self show.
	aBlock value.
	old show


이를 다양한 곳에서 사용했는데, 파일이 확실히 닫히도록 만들 때 사용하곤 한다.

File>>openDuring: aBlock
	self open.
	aBlock value.
	self close


때로는 예외 핸들러(exception handler)에서 Block 평가를 둘러싼 후 (wrap) 두 번째 메시지가 전송되도록 만들고 싶은 경우도 있을 것이다.

File>>openDuring: aBlock
	self open.
	[aBlock value] ensure: [self close]


Execute Around 메서드를 그것이 캡슐화하는 연산의 이름을 딴 메서드 프로토콜에 넣어라. 예를 들어, File>>openDuring: 은 "opening(여는 중)" 이라는 메서드 프로토콜로 들어간다.


메서드에 Intention Revealing Selector(p.49)를 제공할 필요가 있다.


Debug Printing Method (디버그 인쇄 메서드)

  • 기본 인쇄 메서드를 어떻게 코딩하는가?


스몰토크는 객체를 인쇄 가능한 문자열로 전환하는 데에 하나의 메커니즘을 제공한다; printOn:. Strings는 일반 인터페이스에 잘 들어맞으므로 양호하다; 리스트(lists)는 문자열을 표시한다; 테이블(tables)는 문자열을 표시한다; 텍스트 에디터(text editors)와 입력 필드는 문자열을 표시한다.


문자열은 일반 프로그래밍 도구, 즉 Inspector와 같은 도구에서 유용하다. 프로그래머로서 당신은 객체가 생성한 문자열만 봐도 즉시 문제를 진단할 수 있을 것이다.


객체가 생성한 문자열을 보는 사람은 두 부류, 즉 당신과 당신의 고객이 되는데, 이따금씩 둘의 의견이 충돌하는 때가 있을 것이다. 예를 들어, 당신은 프로젝트의 내적, 구조적 세부내용이 한 곳에 배치되어 원하는 객체를 찾을 때 레이어(layer)를 모두 들춰볼 필요가 없도록 만들고 싶다. 하지만 고객은 객체가 올바르게 작동하고 있다고 가정하고 객체에 외부적으로 관련된 측면들을 문자열로 보길 원한다.


VisualWorks는 object-to-string(객체-문자열) 대화의 두 가지 사용을 구분할 수 있는 매우 의미 있는 절차를 취하였다. 고객이 사용하는 문자열을 원한다면 "displayString" 을 전송하라. 프로그래머가 사용하는 문자열을 원한다면 "printString" 을 전송하면 된다. 인쇄에 단일 메시지만 취급하는 스몰토크의 경우, 전달하는 상대가 누군지 선택할 필요가 있다.


  • printOn:을 오버라이드하여 객체의 구조에 관한 정보를 프로그래머에게 제공하라.


프로그래머가 읽을 수 있도록 관련된것들을 출력:

Association>> printOn: aStream
	aStream
		print: self key;
		nextPutAll: '->';
		print: self value


이 패턴의 한 가지 장점은 모든 사용자 인터페이스 빌더들은 문자열을 얻기 위해 객체로 어떤 메시지를 전송할 것인지 파라미터화하는 방법을 가진다는 점이다. 따라서 리스트를 생성하여 몇몇 객체로 전송하면서 "...and send the message 'userString' to the objects to get strings('userString' 이란 메시지를 객체로 전송하여 문자열을 얻는다)" 라고 말할 수 있는 것이다.


Printing Methods는 "printing" 이라는 메서드 프로토콜에 넣어라.


Method Comment (메서드 주석)

Composed Method(p.21)를 작성하였다.

  • 메서드에 어떻게 주석을 달 것인가?


어셈블리 언어 프로그래밍을 사용하던 시절, 프로그래머로서 의도한 것과 그 의도를 컴퓨터가 표현하도록 강요하던 방식 간에는 엄청난 차이가 있었다. 몇 행마다 (때로는 모든 행마다) 다음에 오는 명령어가 의미하는 바를 이해시키기 위해 이야기가 필요했다.


프로그래밍 언어가 발전하면서 표현식도 실제 의미에 가까워지기 시작했고, 몇 개의 행마다 늘어놓던 주석도 줄어들었다. 다수의 주석 기준은 프로시저의 시작점에 위치한 주석부터 프로시저의 목적을 설명하고 argument와 리턴값을 기술한다.


필자는 이러한 "템플릿(template)" 주석에 의미가 없다고 본다. 최근 누군가 필자에게 "당신 메서드 중 주석이 있는 메서드는 몇 퍼센트가 되나요?"라고 단도직입적으로 물어본 적이 있길래 "0에서 1퍼센트 사이일 겁니다,"라고 답했다. 그리고 엄처난 논란이 일어났다! 이를 뒷받침하기 위해 필자는 한 고객의 회사에 근무하는 개발자에게 (이러한 패턴들의 초기 버전을 기반으로 스몰토크를 교육시킨 적이 있다) 200개나 되는 그들의 클래스 시스템에서 주석이 있는 메서드의 비율이 얼만지 물었더니 "0에서 1퍼센트 사이"라고 답했다. "문제가 발생한 적이 있습니까?"라고 물었더니, "단 한 번도 없습니다,"라고 그는 답했다.


수년간 필자는 "자체 문서화(self documenting)" 코드에 대한 과장된 주장들을 확실히 들어왔다. Shoot, Forth는 자체 문서화를 하도록 되어 있다. 이러한 패턴으로 작성된 스몰토크 코드, 즉 뒷받침하는 설명 없이 전략적 정보를 전달하도록 해주는 코드는 대체 어떻게 생겼을까?


"템플릿" 주석의 정보는 다양한 패턴으로 이루어진 코드에서 포착된다; Intention Revealing Selector는 메서드가 하는 일을 전달하고; Type Suggesting Parameter Name은 무엇이 argument가 될 것인지를 말해주며; 다양한 메서드 패턴 타입들은 리턴 타입을 제시하는데, 예로 부울 값(Boolean)을 리턴하는 메서드로 Query Method를 들 수 있겠다.


프로시저에 대해 통신하는 데에 또 다른 중요한 주제가 있다-용도에 따라 코딩된 다양한 사례를 어떻게 처리하느냐는 것이다. 스몰토크에서 중요한 사례들은 당연히 객체가 되고 (아래 Choosing Message 참고), 각 메서드는 단일 사례만 계산한다. 그 결과 필요한 모든 전술적 정보를 독자에게 전달하는 코드가 생긴다.


시스템이 전체로서 얼마나 잘 합해지는지와 상관없이 큰 그림은 메서드별로 쉽게 읽을 수가 없다. 독자에게 시스템을 전체로서 교육하는 다른 방법이 있어야 한다. 클래스와 패키지 주석도 급한 경우에 사용하면 되겠지만 필자는 literate 프로그램을 사용한다. 하지만 아키텍처에 대한 설명을 억지로 메서드 주석으로 집어넣으면 잘 작동할 가능성이 적은데, 독자가 우연히 발견할 가능성이 없기 때문일 것이다.


  • 코드에서 명확하지 않은 중요한 정보는 메서드의 시작점에서 주석으로 전달하라.


코드만을 통해 전달하기 힘든 정보에 대한 예제를 몇 가지 들어보겠다:


  • 메서드 의존성 - 메서드가 올바르게 실행되려면 다른 메서드를 호출해야 하는 경우가 있다. 주석으로 독자에게 특정 메서드 없이 호출하지 말 것을 경고할 수 있다. 이와 같은 정보를 전달하기 위해 Composed Method 또는 Execute Around Method를 사용할 수도 있다.
  • To-do - 필자는 놓치기 싫은 생각을 상기시키기 위해 프로토타이핑(prototyping)하는 동안 주석을 적곤 한다. "효율성을 위해 나중에 Dictionary 사용법 보기," 를 예로 들 수 있겠다. 나중에 다시 생각나면 주석의 내용을 확인하고 결정한 후 주석을 삭제한다.
  • 변경 이유, 특히 기반 클래스 - 무언가 변경해야 하는 경우, 변경 원인을 코드에 직접 나타내는 경우는 드물다. 특히 스몰토크 판매자가 공급한 메서드를 변경할 때 그러하다. 이렇게 코드를 통해 말할 수 없다면 주석을 사용하여 당신이 작업한 내용과 이유를 독자가 이해하도록 도울 수 있다.


쓸모없는 주석을 가장 잘 보여주는 예는 다음과 같다:

(self flags bitAnd: 2r1000) = 1 Am I visible?
        ifTrue: [...]


Composed Method를 재빠르게 확인하면 다음과 같다:

isVisible
	^(self flags bitAnd: 2r1000) = 1


원본 코드는 다음이 된다:

self isVisible
	ifTrue: [...]


이 패턴에 의구심을 가져보길 바란다. 자신만의 개인 작업장에서 실행해볼 수 있는 실험을 알려주겠다. 메서드마다 주석이 달린 코드를 작성하라. 하나씩 메서드를 확인하고 코드에서 알려주는 내용과 정확히 중복되는 주석만 제거해보라. 주석을 삭제할 수 없거든 이러한 패턴들을 이용해 (Composed Method와 Intention Revealing Selector가 특히 유용함) 동일한 내용을 전달하도록 코드를 리팩토링할 수 있는지 확인하라. 여기까지 완료하면 주석이 거의 남아 있지 않을 것이라고 장담한다.


client 의 코드를 마지막 예로 들면 같다:

Bin>>run
	"Tell my station to process me."
	self station process: self


코드를 주석으로 직접 번역할 수 있다:

English Code
Tell my station self station
to process process:
me self


Notes