SmalltalkBestPracticePatterns:4.2
- 4.2 인스턴스 변수
인스턴스 변수
사실 인스턴스 변수와 관련된 절을 작성하기 전에 임시 변수를 먼저 작성하였다. 그런데 임시 변수와 관련된 내용의 결과가 흥미로웠다. 이번 절도 일반적인 "진부한" 패턴 리스트가 나올 것으로 예상했는데, 웬걸! 예상과 달랐다.
문제는 임시 변수가 전적으로 코딩과 관련되었다는 점이다. 임시 변수는 전략적 문제에 대한 전략적 해결책이었다. 따라서 이번 책의 범위에 완벽하게 맞아 떨어졌다.
인스턴스 변수의 사용엥서 대부분은 전략적인 사용에 해당하지 않는다. 계산의 책임 분배는 물론이고 모델을 어떻게 표현하는지에 대한 선택은 모델링에서 핵심이다. 인스턴스 변수의 생성 결정은 주로 임시 변수의 생성 결정과는 완전히 다른 사고방식과 맥락에서 비롯된다.
이번 절을 남겨두는 이유는 인스턴스 변수를 생성해야 하는 코딩과 관련된 중요한 이유들이 여전히 존재하며, 인스턴스 변수를 사용할 때 학습해야 할 실용적이고 전략적인 기법들이 있기 때문이다.
Common State (공통 상태)
상태, 즉 클래스의 모든 인스턴스에 존재하게 될 다른 값들을 어떻게 표현하는가?
계산 시간의 초기에는 상태만이 존재했다. 상태만 보이는 펀치 카드(punched card) 상자는 무엇일까? 유닛 레코드(unit record) 기기가 존재하여 상태를 재정렬(re-order)하고 변형(transfer)했지만 모든 변형은 기계로 하드 코딩되었다 (전선, 도르래, 기어 등으로). 그 때 상태는 그야말로 왕이었다. 전자계산기가 실행한 첫 번째 작업은 상태를 가상으로 만드는 것이었다. 물리적 형태로 존재할 뿐 아니라 물리적 형태가 전자(electron)로 바뀌어 더 쉽고 빠르게 조작이 가능해졌다. 조작은 여전히 패치 코드(patch cord) 형태로 물리적으로 표현되었지만 변경이 훨씬 수월해졌다.
그러다가 내장식 프로그램 컴퓨터는 이 모든 것을 바꾸어놓았다. 이제 조작과 상태가 동등해졌다. 둘 다 동일한 기계의 가상 부분으로, 쉽게 변경이 가능하도록 저장되었다.
초기 기계들의 자원은 극단적으로 제한되어 있었기 때문에 프로그램과 상태의 결합이 완전해졌다. 때로는 프로그램을 데이터로 취급할 의향이 있다면 프로그램을 몇 바이트만으로 인코딩이 가능했다ㅡ상황에 따라 프로그램을 변경하면서. 물론 프로그램 소스를 읽기만해서는 무슨 일이 일어나는지 알아낼 수는 없었고, 이해를 위해선 많은 상태에서 실행되는 프로그램을 관찰해야만 했다.
메모리가 충분해지자 더 이상 공간 효율성을 빌미로 한 잔혹한 행위를 행할 필요가 없어졌지만 상태는 여전히 우리를 괴롭혔다. 수많은 함수들이 수많은 상태 비트를 사용한 거대한 프로그램들이 작성되었다. 어떤 상태의 부분도 프로그램의 수많은 부분들을 변경하지 않고서는 변경이 불가했다.
그러한 프로그램의 엄청난 비용은 반발을 일으켰다. 상태로서의 프로그램은 양호하지 못했다. 상태 또한 양호하지 못했을 터이다. 이로 인해 상태는 존재하지 않고 프로그램만이 존재하는 함수형 프로그래밍이 개발되었다.
개념적, 수학적 우수성에도 불구하고 함수형 프로그래밍 언어는 상업적 소프트웨어에서 인기를 얻지 못했다. 문제는 아직도 프로그래머들이 상태와 관련해 사고하고 모델링한다는 점이었다. 상태는 세계를 생각하는 참 좋은 방식이다.
객체들은 절충안을 나타낸다. 상태는 훌륭하지만 적절하게 관리할 때만 그러하다. 상태라 함은 매우 작은 조각으로, 그 중 일부는 비슷하게, 일부는 서로 다르게 나누어야만 관리가 가능하다. 이와 비슷하게, 서로 관련된 프로그램들도 여러 조각으로 나누고, 그 중 일부는 비슷하게, 일부는 다르게 나눈다. 이렇게 할 때 프로그램 상태의 표현 중에서 일부를 변경하더라도 프로그램 내에서 국소화된 부분으로 변경을 제한할 수 있다.
- 클래스에서 인스턴스 변수를 선언하라.
Cartesian point는 가로 및 세로 원점(offset)을 보유하는 인스턴스 변수를 갖는다. 극좌표에서 Point는 반경(radius) 및 각도(angular) 오프셋을 보유하는 인스턴스 변수를 갖는다. House는 그것의 모든 방(room)의 컬렉션을 보유하는 인스턴스 변수를 갖는다.
인스턴스 변수는 매우 중요한 통신 역할을 한다. 공개적으로 한 번만, 그것도 "이 객체로 모델링하는 것은 다음과 같습니다,"라고 말한다. 객체 집합은 인스턴스 변수가 무엇인지, 이름을 어떻게 지었는지를 통해 본래 프로그래머의 의도를 많이 알려준다.
클래스 정의에서는 인스턴스 변수의 중요도 순으로 선언할 것을 명심하라.
Role Suggesting Instance Variable Name(p.110)를 이용해 명명하라. 인스턴스 변수의 초기화에는 Lazy Initialization (p.85), Explicit Initialization (p.83), Constructor Parameter Method (p.25), 셋 중 하나를 사용하라. 변수에 Direct Variable Access (p.89) 또는 indirect Variable Access (p.91) 중 어느 것을 사용할 것인지 선택하라.
Variable State (변수 상태)
- 인스턴스마다 존재가 다양한 상태를 어떻게 표현하는가?
경고! 이것은 자주 사용하는 패턴이 아니다. 이 내용을 싣는 이유는 이것으로 작성된 코드를 언젠간 마주칠 것이고, 무슨 일이 일어나는지 이해해야 할 것이기 때문이다.
이 패턴은 스몰토크로 입문한 LISP 프로그래머들을 위한 패턴이다. 일부 LISP 문화에서는 무엇보다 유연성을 중시한다. 그들의 객체는 고정된 부분이 전혀 없는 경향을 가진다. Point에 x가 있을 수도, y가 있을 수도, 또는 색상이 있을 수도 있는데, 누가 알겠는가?
이와 같은 코드의 증상으로는 클래스들이 Objects에 대한 Strings 또는 Symbols 의 (변수의 이름) Dictionary 매핑을 보유하는 단일 인스턴스 변수를 선언한다는 것이다. Dictionary 값 중 일부는 Numbers가 될 수도 있고, 그 외는 Collections, 또 다른 값은 Strings가 될 수도 있다. 게다가 이 클래스의 여러 인스턴스를 보면 모든 Dictionaries가 동일한 키(key)를 갖는다.
이와 같은 코드에서 문제는, 코드를 읽을 수도 없으니 무슨 일이 일어나는지 이해할 수가 없다는 점이다. 프로그래밍 툴은 이러한 스타일의 프로그래밍을 잘 지원하지 않는다. Dictionaries에 모든 상태를 저장하는 코드를 위한 "인스턴스 변수 참조 보기"가 없다.
심지어 동일한 클래스의 인스턴스에서도 상태가 어떤 때는 표현되고 어떤 때는 표현되지 않는 모델을 빌드하는 것은 확실히 타당한 방법이다. 소수의 인스턴스가 nil 값을 필요로 한다고 해서 거의 모든 인스턴스에 nil 값으로 된 수백 개의 변수를 선언하기는 원치 않을 것이다.
- 일부 인스턴스만 갖게 될 변수들을 "properties"라 불리는 인스턴스 변수에 저장된 Dictionary에 넣어라. 프로퍼티에 접근하기 위해선 "propertyAt: aSymbol"과 "propertyAt: aSymbol put: anObject"를 구현하라.
Visual Smalltalk는 프로그램의 다양한 부분들 간 통신을 위해 그 그래픽 위젯에 연결된 프로퍼티들을 사용한다. 예를 들어, EntryField는 필드의 내용이 변경되었는지 결정하는데 사용되는 'PreviousContents'라는 이름의 프로퍼티를 갖고 있다.
VisualWorks 2.5는 이러한 패턴을 언제 사용하면 안 되는지에 대해 완벽한 예제를 포함한다. 그것의 Package 객체는 Variable State를 사용한다. Constructor Method는 Package에 대한 이름을 취하므로 모든 Package에는 이름이 있어야 한다 (공통 상태). 그렇지만 인스턴스 변수에 저장되는 대신 변수 상태의 일부로서 저장된다.
어떤 변수든, 모든 또는 거의 모든 공유(shared) 인스턴스는 프로퍼티 리스트 상의 엔트리가 아니라 인스턴스 변수로서 확실히 구현되도록 하라. Variable State(변수 상태)의 보편성(generality)은 사용되지 않지만 그 자체는 임시적 편의성을 위해 종종 사용되며 절대 재고(revisit)하지 않는다.
인스턴스 변수로서 저장된 변수와 프로퍼티 리스트 상에 저장된 변수 간에 차이는 Getting Method (p.93) 또는 Setting Method (p.95)를 이용해 숨기고 싶을 것이다.
Explicit Initialization (명시적 초기화)
항상 동일한 값에서 시작하는 공통 상태(p.80)를 발견하였다. Constructor Parameter Method (p.25)가 방금 일부 변수의 상태를 설정하였다.
- 인스턴스 변수를 어떻게 기본 값으로 초기화하는가?
필자는 보통 이러한 패턴에서는 모호함을 피하려고 한다. 어떤 작업을 수행하는 두 가지 타당한 방법이 있다면 하나를 고른다.
초기화는 필자의 양심상 하나의 해결책만 선택할 수 없는 주제이다. 인스턴스 변수 초기화 문제를 해결하는 방법에는 상황에 따라 - 확실히 직면할 것이라 생각되는 상황 - 유효한 두 가지 방법이 있다.
이 패턴은 유연성보다 가독성을 강조한다. 당신은 사람들이 자신의 코드를 가능한 쉽게 읽길 바랄 것이며, 향후에 서브클래스를 그다지 염려하지 않아도 되도록 메서드를 초기화해야 한다. 명시적 초기화는 모든 initialization부를 한 곳에 위치시킴으로써 모든 변수들이 무엇인지 쉽게 확인할 수 있도록 해준다.
모든 변수는 단일 메서드 내에서 언급되므로 명시적 초기화는 유연성을 해친다. 인스턴스 변수를 추가하거나 제거할 경우 명시적 초기화를 편집할 것을 기억해야 한다.
명시적 초기화는 또한 느긋한 초기화(Lazy Initialization)보다 비용이 더 많이 드는데, 이유는 인스턴스 생성 시에 모든 변수를 초기화하려고 전력을 다하기 때문이다. 초기 값 일부를 계산하는 데에 비용이 많이 들었는데 간혹 올바르게 사용되지 않을 (또는 전혀 사용되지 않을) 경우, 즉시 초기화를 피함으로써 성능을 향상시킬 수 있다.
- 모든 값을 명시적으로 설정하는 "initialize" 메서드를 구현하라. 클래스 메시지 "new"를 오버라이드하여 새로운 인스턴스에서 호출할 수 있도록 하라.
예를 들어, 기본 값으로 매 1000밀리초마다 실행되는 Timer가 있다고 치자. 몇 회를 울렸는지도 추적한다.
Class: Timer
superclass: Object
instance variables: period count
향후 슈퍼클래스가 이를 호출하면 우연히 "initialize"를 두 번 설정하는 일을 피하기 위해 basicNew를 이용해 인스턴스를 생성하라.
Timer class>>new
^self basicNew initialize
이제 값을 초기화할 수 있다:
Timer>>initialize
count := 0.
period := 1000
더 좋은 점은 아래 메시지로 마법의 숫자 1000을 설명할 수 있다는 사실이다:
Timer>>defaultMillisecondPeriod
^1000
Timer>>initialize
count := 0.
period := self defaultMillisecondPeriod
#initialize 메서드를 "initializerelease"라 불리는 메서드 프로토콜에 넣어라.
분명하지 않은 값을 설명하려면 Default Value Method(p.86)를 사용하라. 계층구조 내 Initialize Method(초기화 메서드)들은 공통적으로 작업을 완료하기 위해 Extending Super(p.60)를 필요로 한다.
Lazy Initialization (느긋한 초기화)
공통 상태(p.80)를 초기화하고 있다.
- 인스턴스 변수를 어떻게 기본 값으로 초기화하는가?
여기 변수 초기화의 다른 면이 있다. 명시적 초기화의 모든 장점은 약점이 되고 모든 약점이 강점이 된다.
느긋한 초기화는 initialization을 변수마다 두 개의 메서드로 나눈다. 첫 번째 부분은 변수가 느긋하게 초기화된다는 사실을 그 값을 사용하길 원하는 이로부터 숨기는 Getting Method이다. 이는 Direct Variable Access가 존재할 수 없음을 암시한다. 두 번째 메서드, Default Value Method는 변수의 기본 값을 제공한다.
여기서 유연성이 개입한다. 서브클래스를 만들 경우, Default Value Method를 오버라이드함으로써 기본 값을 변경할 수 있다.
단, 모든 변수와 그 초기 값을 한 번에 살펴볼 수 있는 하나의 위치가 존재하지 않기 때문에 가독성을 해친다. 대신 모든 값을 확인하려면 여러 메서드를 살펴봐야 한다.
느긋한 초기화를 사용하는 또 다른 이유로 성능을 들 수 있다. 한동안 (또는 전혀) 필요 없는 값비싼 초기화는 느긋한 초기화를 이용해 연기시킬 수 있다.
- 변수에 대해 Getting Method를 작성하라. 필요 시 Default Value Method를 이용해 초기화하라.
위의 Timer 예제는 명시적 초기화가 필요 없다. 대신 계수(count)가 그것의 Getting Method에서 초기화된다:
Timer>>count
count isNil ifTrue: [count := self defaultCount].
^count
기본 값은 그 고유의 메서드로부터 비롯된다:
Timer>>defaultCount
^0
Period도 이와 비슷하다:
Timer>>period
period isNil ifTrue: [period := self defaultPeriod].
^period
Timer>>defaultPeriod
^1000
어떤 사람들은 느긋한 초기화를 맹신하기도 한다. 그들은 모든 초기화를 이러한 방식으로 행해야 한다고 말하기까지 한다. 사실 필자는 명시적 초기화에 관심이 기운다. 얼마나 많은 시간 동안 사용하지도 않을 유연성을 제공하기 위해 노력했던가. 필자라면 우선 간단한 방식을 사용한 후에 문제가 되면 고치는 쪽을 택하겠다. 하지만 타인이 서브클래싱할 가능성이 있는 코드를 작성하고 있다면 애초에 느긋한 초기화를 사용하겠다.
Default Value Method(p.86)는 기본 값을 제공한다. Compute Method는 Caching Instance Variable의 값을 제공한다.
Default Value Method (기본 값 메서드)
명시적 초기화(p.83)에 복잡한 기본 값이 있다. 느긋한 초기화(p.85)에 대한 기본 값이 필요하다.
- 변수의 기본 값을 어떻게 표현하는가?
기본 값을 제공하는 가장 간단한 방법은 코드에 맞추는 방법이다. 그래야 읽기가 쉽고, 쓰기도 빠르고 간단하다.
유연성을 생성하는 것이 목적이거나 (느긋한 초기화) 추가적 통신이 필요한 패턴들은 (명시적 초기화) Intention Revealing Message(의도를 알려주는 메시지)가 필요하다. 이는 선택자를 통해 통신할 기회를 제공한다. 또한 단일 값을 단순히 오버라이딩할 수 있는 선택권을 향후 서브클래스에게 제공한다.
- 값을 리턴하는 메서드를 생성하라. 변수 이름 앞에 "default"를 붙여서 메서드를 명명하라.
synopsis를 입력하지 않을 경우 Book는 synopsis에 빈 String을 제공할지 모른다:
Book>>defaultSynopsis
^"
Caching Instance Variable의 값을 계산하고 있다면, "defaultArea" 대신 "computeArea" 메서드를 호출하라. 이는 변수가 캐시의 의도로 한다는 단서를 독자에게 제공할 것이다. Default Value Method는 "private"이라 불리는 메서드 프로토콜에 넣어라.
상수 기본 값에 대해서는 Constant Method(p.87)를 사용하라.
Constant Method (상수 메서드)
Composed Method(p.21)를 작성하고 있다. Default Value Method(p.86)가 필요할지 모른다.
- 상수를 어떻게 코딩하는가?
스몰토크는 공유된 상수 또는 변수를 표현하는 데에 여러 옵션을 제공한다. Pools는 다수의 다른 클래스에서 사용 가능한 변수의 집합을 제공한다. 클래스 변수는 계층구조를 통해 공유되는 변수들이다.
어떤 사람들은 애플리케이션에 수많은 풀(pool)을 사용하기도 한다. 하지만 필자는 전혀 사용하지 않는다. 풀을 싫어하는 이유는, 유용한 책임을 만드는 과정을 너무나 쉽게 포기하기 만들고, 그 대신 단순히 데이터를 이리저리 퍼뜨리는 데에 그치기 때문이다.
IBM 스몰토크 윈도우 시스템이 좋은 예가 된다. 여기엔 수백 개의 상수를 포함하는 CwConstants라 불리는 풀이 있다. 한 번에 하나의 항목을 선택하기 위해 이것은 리스트 패인(list pane)으로 #singleSelect와 같은 메시지를 전송하는 대신 "selectionPolicy: XmSINGLESELECT"를 전송한다.
물론 이 한 가지만 기억하면 된다고 말하는 사람도 있을 것인데, 사실상 C 프로그래머들은 항상 이런 일을 처리해야 한다. 하지만 이렇게 "기억해야 할 한 가지"가 수십만 개가 되어 더 이상 스몰토크의 느낌이 나는 시스템이 없다고 생각해보라.
필자는 오히려 상수의 가시성(visibility)을 하나의 클래스로 제한하고자 할 것이다. 그러면 그 클래스는 다른 객체들에게 의미 있는 행위를 제공할 수 있다. 하나의 클래스가 하나의 상수에만 관심을 가지도록 코드를 재팩토링한다면 그 상수는 메서드로서 표현할 수 있다.
리스트 패인 예제에서 이는 본래 #selectionPolicy: 의 구현부는 그대로 놔두되 외부 객체가 그것을 호출할 것이라 기대하지 않음을 의미한다. 대신, 가능한 argument 값마다 메서드를 생성한다:
ListPane>>singleSelect
self selectionPolicy: 15
다른 메서드들도 15가 하나의 선택에 대한 매직(magic) 상수였음을 알 필요가 있다면, 그에 대한 메서드ㅡ15를 리턴하기만 하는 메서드ㅡ도 생성해야 한다.
ListPane>>singleSelectPolicy
^15
ListPane>>singleSelect
self selectionPolicy: self singleSelectPolicy
상수를 메서드로 표현하는 데에 발생하는 단점 중 하나는 자동으로 메서드를 생성하는 것보다 자동으로 풀에 대한 initialization을 생성하는 편이 훨씬 쉽다는 점이다. 다른 프로그램으로 상수 집합을 동기화해야 하는 경우, 풀을 사용하는 편이 수월하다는 사실을 발견할 것이다. 하지만 여전히 당신은 풍부한 프로토콜을 제공하도록 노력하여 풀에 대해 알고 있는 클래스가 거의 없도록 해야 한다.
Constant Methods에 관한 또 다른 질문은, 수백 또는 수천 개의 상수가 있을 때 무슨 일이 발생하느냐가 되겠다. 풀은 쉽게 증가하여, 필요 시 가능한 한 많은 엔트리를 포함하도록 되어 있다. 수천 개의 Constant Methods가 있는 클래스는 어수선해 보일 것이다.
- 상수를 리턴하는 메서드를 생성하라.
시각적 컴포넌트의 색상을 Constant Methods로서 구현할 수 있다:
SelectorFigure>>textColor
^Color darkGray
ExpressionFigure>>textColor
^Color chartreuse
이 패턴에 관해 반복적으로 들어왔던 견해는, "하나의 클래스에서 수백 개의 상수를 처리해야 한다면 어떻게 하는가?"라는 질문이었다. 수백 개의 상수를 처리해야 하는 클래스는 확실히 너무 많은 작업을 수행하고 있으므로 나눌 필요가 있다고 생각한다. 하지만 불가피하게 그대로 사용해야 하는 경우라면 다른 기능, 즉 Pools와 같은 기능들을 이용해 엄청난 수의 공유 상수를 관리할 수 있다.
Constant Methods를 "private"이라 불리는 메서드 프로토콜에 넣어라.
Direct Variable Access (직접적 변수 접근)
공통 상태(p.80)로 가능한 한 읽게 쉽게 접근할 필요가 있다.
- 어떻게 인스턴스 변수의 값을 얻고 설정하는가?
상태로의 접근은 초기화와 비슷한 주제로, 두 가지 해답이 있다. 하나는 좀 더 읽기가 쉽고, 다른 한 가지는 좀 더 유연하다. 초기화와 마찬가지로 두 가지 접근법의 독단적인 지지자들을 발견할 것이다.
좀 더 간단하고 읽기 쉽게 인스턴스 변수의 값을 얻고 설정하는 방법은, 값을 필요로 하는 모든 메서드에서 직접적으로 변수를 사용하는 것이다. 그리고 두 번째는 인스턴스 변수의 값을 사용하거나 변경할 때마다 메시지를 전송하도록 요구하는 방법이다.
80년대 중반 Tektronix에서 스몰토크로 프로그래밍을 처음 시작했을 당시, 상태 접근법에 관한 논쟁은 매우 뜨거웠다. 양편 모두 자신감에 가득찬 주장을 내세웠다 (이런 연구실 너무 좋지 않은가?).
Indirect Variable Access (간접적 변수 접근) 집단이 결국 스몰토크 군중의 마음과 심장을 사로잡았는데, 필자가 생각하기엔 수많은 훈련기업 중 대다수가 간접적 접근을 가르치기 때문인 것 같다. 그들에게는 "직접적 접근은 나쁘고 간접적 접근은 좋다"라는, 여간 간단한 문제가 아니었다.
스몰토크 리포트 컬럼에서 필자는 몇 년 전에 다시 논쟁을 시작하고자 했다. 소규모의 전투가시작되었지만 흐지부지 끝나버렸다. 필자는 과연 내가 직접적 접근의 장점을 과도하게 부각시키려 한 건 아닌지 궁금했다. 긁어부스럼 만든 건 아닌가 싶었다.
그리고 수 개월간 간접적 접근을 주장하던 고객과 작업을 하게 되었다. 전문 프로그래머는 자신의 수많은 스타일을 마음대로 이용할 수 있었기 때문에 필자는 시키는대로 getters와 setters를 작성했다.
그 고객과 작업이 끝나자 나 자신을 위한 코드를 작성했다. 이전에 작업하던 것보다 어찌나 매끄럽게 읽히는지 보고 놀람을 금치 않을 수 없었다. 유일한 스타일 차이는 직접적인 접근과 간접적인 접근이었다.
필자는, 아래를 읽을 때마다:
...self x...
"x" 메시지가 인스턴스 변수를 페치(fetch)하고 있었음을 상기시키기까지 시간이 어느 정도 소요된다는 사실을 눈치챘다. 그리고 아래를 읽을 때에는:
...x...
멈춤 없이 계속 읽어나갔다.
Ward에게 내 경험을 말해주었더니 훌륭한 해석을 해주었다. 소량의 메서드를 갖는 클래스를 쓸 때는 메서드의 getting과 setting이 당신의 클래스 내에서 메서드 수를 손쉽게 두 배로 증가시킬 수 있다ㅡ사용하지 않을 유연성을 제공하기 위해 메서드가 두 배나 증가하는 셈.
반면 Direct Variable Access(간접적 변수 접근)를 사용했는지 알아보기 위한 목적으로 누군가에게서 클래스를 얻어 재빠르게 서브클래싱할 기회를 찾기란 당혹스러운 일이므로, 슈퍼클래스 내 코드 대부분을 변경하지 않고선 변경하는 방법이 없다.
- 변수에 직접적으로 접근하여 설정하라.
이 책에 예제 코드 대부분은 Direct Variable Access를 사용하는데, 가능한 한 읽기 쉽도록 만들고 싶기 때문이다. 직접적 접근을 갖고 있다면 Indirect Variable Access(간접적 변수 접근)에 넣기가 어렵지 않다. 따라서 다음을 재빠르게 읽을 수 있으며:
Point>>setX: xNumber y: yNumber
x := xNumber.
y := yNumber
추후 간접 접근을 선택할 경우 무엇을 할지 모르는 위험이 없다.
Indirect Variable Access (간접적 변수 접근)
공통 상태(p.80)로 가능한 한 유연하게 접근할 필요가 있다.
- 어떻게 인스턴스 변수의 값을 얻고 설정하는가?
이제 본격적으로 필자의 정신분열증을 표출할 때가 되었다. Direct Variable Access(직접적 변수 접근)에서 변수의 사용만으로 충분하다고 설득한 마당에 필자는 감히 요청하건대 그따위 것은 무시하고 이제부터 하는 말을 잘 읽어보길 권한다.
Direct Variable Access(직접적 변수 접근)를 사용할 때는 다수의 메서드들이 변수의 값이 유효하다는 가정을 내린다. 일부 코드에서는 합당한 가정이다. 하지만 값의 저장을 중단하고 계산을 시작하기 위해 느긋한 초기화를 도입하고 싶다거나, 새 서브클래스에서 가정을 변경하고 싶은 경우라면 유효성에 대한 가정이 너무나 널리 퍼졌다는 사실에 실망할 것이다.
이에 대한 해결책은, 변수 접근에 항상 Getting Methods와 Setting Methods를 사용하는 것이다. 따라서:
Point>>+ aPoint
^x + aPoint x @ (y + aPoint y)
위의 코드 대신 아래가 보일 것이다:
Point>>+ aPoint
^self x + aPoint x @ (self y @ aPoint y)
Indirect Variable Access를 사용하는 클래스의 인스턴스 변수 참조를 보면, 각 변수에 대해 두 가지 참조, Getting Method와 Setting Method만 볼 것이다.
Indirect Variable Access(간적접 변수 접근)를 이용 시 단순성과 가독성은 포기해야 하므로 당신이 모든 Getting Methods와 Setting Methods를 정의해야 한다. 시스템이 프로그래머를 대신해 정의해준다 하더라도 관리 및 문서화를 위해선 그만큼 많은 메서드가 있어야 한다. 앞서 설명한 바와 같이, Direct Variable Access(간접적 변수 접근)를 이용한 코딩은 부드럽게 읽히지만 "그렇지, 저건 그냥 Getting Method야,"라고 영원히 스스로 상기시킬 수는 없기 때문이다.
- Getting Method와 Setting Method를 통해서만 그 값에 접근하고 설정하라.
Indirect Variable Access(간접적 변수 접근)로 작성된 Point는 그 인스턴스 변수에 대한 메서드를 제공한다:
Point>>x
^x
Point>>x: aNumber
x := aNumber
이 방식을 통해 당신은 이러한 메서드들을 서로 다르게 구현하는 데에 그치는 PolarPoint 서브클래스를 정의할 수 있으며 모든 슈퍼클래스는 그대로 작동할 것이다.
PolarPoint>>x
^self radius cos * self theta
상속을 위한 코딩이 필요한 경우 Indirect Variable Access(간접적 변수 접근)를 사용하라. 후손들이 매우 고마워할 것이다.
한 가지 경고를 하자면, 중간에 흐지부지 하지마라. 간접 접근을 사용할 요량이라면 모든 접근에 사용하라. 가령 추후에 느긋한 초기화의 장점을 얻기 위해 변수를 간접 접근으로 옮길 필요가 있다고 결정할 경우, 그 시간 내내 변수의 접근을 변경하라. 반면, 한 객체에서 일부 변수는 직접적으로 접근되고 나머지 변수들은 간접 접근되는 모습도 그다지 나쁘지 않다.
각 변수마다 Getting Method(p.93)와 Setting Method(p.95)를 정의할 필요가 있을 것이다. 컬렉션을 보유한 변수들의 경우, Collection Accessor Methods(p.96)와 Enumeration Method(p.144)의 구현을 고려해보라.
Getting Method
느긋한 초기화 또는 Indirect Variable Access(p.91)을 사용하고 있다.
- 인스턴스 변수로의 접근성은 어떻게 제공하는가?
Indirect Variable Access(간접적 변수 접근)를 사용하기로 결정했다면 당신은 변수 값을 얻고 설정하기 위한 메시지 기반의 프로토콜을 제공할 의무가 있다. 그렇다면 유일하게 답해야 할 질문은 그것을 어떻게 사용하고 명명하느냐가 된다.
훌륭한 Getting Methods를 작성하는 비밀은 다음과 같다 - 처음에 private으로 만들어라. 이 점은 아무리 강조해도 지나치지 않는다. 동일한 객체가 Getting Method를 호출하고 구현하기만 한다면 문제가 없을 것이다. 다시 말하자면, 당신은 항상 Getting Methods를 호출하는 메시지들을 "self"로 전송해야 한다.
이를 목적으로 어떤 사람들은 메서드의 이름 앞에 "my"를 붙이려고 한다. 따라서:
x
^x
위의 것이 아니라 아래를 갖게 된다:
myX
^x
따라서 아래와 같이:
self bounds origin myX
참 어리석은 코드를 만든다. 필자는 사실 이럴 필요가 없다고 생각한다. 그보다 프로그래머에게 (필자를 포함해) 해석의 여지를 제공하는 편이 낫다. 일부 다른 객체가 꼭 필자의 private Getting Method를 전송해야 한다면, 가능한 한 읽기 쉬운 방식으로 이루어져야 한다.
- 변수의 값을 리턴하는 메서드를 제공하라. 변수와 동일한 이름을 제공하라.
사용자 인터페이스에 저자와 제목을 표시할 필요가 있는 Book는 Getting Methods를 publish할 것이다:
Book>>author
^author
Book>>title
^title
외부 세계에서의 사용을 위해 Getting Methods의 존재를 publish해야 하는 경우가 있다. 따라서 당신은 가능한 모든 대안책을 고려한 후에 의식적인 선택을 내려야 한다. 객체를 데이터 구조로서 행위하도록 두기보다는 객체에 더 많은 책임을 부여하는 편이 낫다.
Private Getting Methods는 "privateaccessing"이라 불리는 메서드 프로토콜에 넣어라. 그리고 public Getting Methods는 "accessing"이라 불리는 메서드 프로토콜에 넣어라.
Setting Method
Indirect Variable Access(p.91)를 사용하고 있다.
- 인스턴스 변수의 값은 어떻게 변경하는가?
앞에서 Getting Methods에 대해 언급한 말들은 모두 Setting Methods에 적용시키고 싶다. 단 Setting Methods는 그보다 더 private해야 한다. 다른 객체가 당신의 상태를 뜯어내도록 만드는 것과 그것을 다른 상태로 부수는 것은 또 다르다. 혼란스럽겠지만 두 개의 객체 내 코드가 부조화스럽게 부숴질(break) 가능성은 매우 많다.
다시 명명으로 돌아가보면, Setting Methods의 이름 앞에 "my"를 굳이 붙일 필요가 없다고 생각한다. 승인되지 않은 사용으로부터의 좀 더 보호해주긴 하지만 가독성을 희생해야 할만큼의 가치는 없다.
- 메서드에 변수와 동일한 이름을 제공하라. 단일 파라미터를 취하고 값이 설정되도록 하라.
Book 편집 인터페이스는 작가와 제목에 대한 Setting Methods를 필요로 할 것이다:
Book>>author: aString
author := aString
Book>>title: aString
title := aString
Indirect Variable Access(간접적 변수 접근)를 사용한다 하더라도, 인스턴스 생성 시에만 설정되는 하나의 변수만 가진 경우, 개인적으로 Setting Method를 제공하지 않겠다. 필자라면 모든 값을 한 번에 설정해주는 Constructor Parameter Method를 사용하겠다. Setting Method가 있다면 변수에 대한 모든 변경에 대해 사용해야 한다.
Private Setting Methods를 "privateaccessing"이라 불리는 메서드 프로토콜에 넣어라. Public Setting Methods는 "accessing"이라 불리는 메서드 프로토콜에 넣어라.
부울식 프로퍼티는 Boolean Property Setting Method(p.100)로 설정하라.
Collection Accessor Method (컬렉션 접근자 메서드)
Indirect Variable Access(p.91)를 사용하고 있다.
- 컬렉션을 보유하는 인스턴스 변수로 어떻게 접근성을 제공하는가?
가장 간단한 방법은 변수에 대해 Getting Method를 publish하는 방법으로, 이를 통해 원한다면 어떤 고객이든 컬렉션으로 추가, 컬렉션으로부터 삭제, 반복(iterate over), 또는 사용할 수 있다.
이 접근법의 문제는 객체의 구현부를 외부 세상으로 너무 많이 노출시킨다는 점이다. 객체가 가령 다른 종류의 컬렉션을 사용하는 등 구현부를 변경하기로 결정하는 경우, 고객의 코드는 잘 부서질 수 있다.
Private 컬렉션을 대중에게 제공 시 발생하는 또 다른 문제점으로는 타인이 당신에게 통지하지 않은 채 컬렉션을 변경하는 경우 그와 연관된 상태를 최신으로 유지하기가 힘들단 점이 있다. 아래는 총 급여로 빠르게 접근하기 위해 Caching Instance Variable을 사용하는 부서이다:
Department
superclass: Object
instance variables: employees totalSalary
totalSalary
totalSalary isNil ifTrue: [totalSalary := self
computeTotalSalary].
^totalSalary
computeTotalSalary
^employees
inject: 0
into: [:sum :each | sum + each salary]
clearTotalSalary
totalSalary := nil
직원이 속한 부서에 알리지 않은 채 직원을 바로 삭제하는 고객 코드의 경우 어떤 일이 발생할까?[1]
...aDepartment employees remove: anEmployee...
totalSalary 캐시(cache)는 절대 제거되지 않는다. 이제 computeTotalSalary가 리턴시키는 값에 일치하지 않는 숫자를 포함하게 된다.
이를 해결하기 위해선 다른 객체들이 당신의 컬렉션을 갖지 않도록 해야 한다. Indirect Variable Access(간접적 변수 접근)를 사용한다면 컬렉션을 대상으로 한 Getting Methods가 private하도록 확보하라. 대신 당신이 구현한 메시지를 통해 고객에게 컬렉션을 대상으로 한 연산(operations)으로 제한된 접근성만 제공하라. 이는 당신이 필요로 하는 기타 처리를 실행할 수 있는 기회를 제공한다.
이 접근법의 단점은 이러한 메서드를 당신이 직접 구현해야 한다는 점이다. 컬렉션으로 접근을 제공하는 데에 하나의 메서드가 필요하다. 컬렉션에 필요한 보호된 접근성을 제공하는 데에도 4~5개의 메서드가 필요할 것이다. 장기간으로 볼 때는 그만한 가치가 있는데, 이는 코드가 훨씬 더 쉽게 읽히고 변경 가능하기 때문이다.
- Delegation으로 구현되는 메서드를 컬렉션에 제공하라. 메서드의 이름을 정하려면 컬렉션 이름을 컬렉션 메시지로 추가하라.
Department가 다른 사람들이 직원을 추가하거나 제거할 수 있도록 허용하는 경우, Collection Accessor Methods(컬렉션 접근자 메서드)를 구현할 것이다:
Department
superclass: Object
instance variables: employees totalSalary
totalSalary
totalSalary isNil ifTrue: [totalSalary := self
computeTotalSalary].
^totalSalary
computeTotalSalary
^employees
inject: 0
into: [:sum :each | sum + each salary]
clearTotalSalary
totalSalary := nil
위임하는 컬렉션 메시지의 이름을 무작정 따서 Collection Accessor Method를 명명하지 마라. 도메인에 더 알맞은 단어가 있는지 찾아보라. 예를 들면:
includesEmployee: anEmployee
^employees includes: anEmployee
위의 그림보단 아래를 선호하는 편이다:
employs: anEmployee
^employees includes: anEmployee
Collection Accessor Methods는 "accessing"이라 불리는 메서드 프로토콜에 넣어라.
안전하고 효율적인 일반 컬렉션 접근성을 위해 Enumeration Method(p.99)를 구현하라.
Enumeration Method (열거 메서드)
Indirect Variable Access(p.91)를 사용하고 있다. Collection Accessor Method(p.96)를 구현했을 수 있다.
- 컬렉션 요소로 안전하고 일반적인 접근성을 제공하는 방법은?
때때로 고객들은 private 컬렉션으로 접근하는 다수의 방법을 원할 때가 있다. 20개 또는 30개가 되는 Collection Accessor Methods를 구현할 수는 있지만 그만큼 시간적 여유가 없고, 그 수로 충분할지 확신도 없다. 뿐만 아니라 컬렉션에 대한 Getting Method를 public으로 만들지 않기 위한 모든 arguments 또한 여전히 존재한다.[2]
- 컬렉션 각 요소마다 Block을 실행하는 메서드를 구현하라. 메서드 이름은 컬렉션 이름과 "Do:"를 붙여서 만들어라.
Department의 Employees에 대한 Enumeration Method는 다음과 같은 모습일 것이다:
Department>>employeesDo: aBlock
employees do: aBlock
이제 수많은 부서에서 모든 Employees의 컬렉션을 얻고자 하는 고객 코드는 Concatenating Stream을 사용할 수 있겠다:
allEmployees
| writer |
writer := WriteStream on: Array new.
self departments do: [:eachDepartment |
eachDepartment employeesDo: [:eachEmployee | writer
nextPut: eachEmployee]]
^writer contents
만일 Departments가 Employees 뿐만 아니라 다른 Departments를 포함할 수 있도록 만들고 싶다면 (이는 Composite 모델링 패턴의 예제이다) 어떻게 될까? 둘 다를 대상으로 employeesDo:를 구현할 수 있다:
Department>>employeesDo: aBlock
employees do: [:each | each employeesDo: aBlock]
Employee>>employeesDo: aBlock
aBlock value: self
Enumeration Methods를 "enumerating"이라 불리는 메서드 프로토콜에 넣어라.
Boolean Property Setting Method (부울 프로퍼티 설정 메서드)
Setting Method(p.95)를 사용하고 있다.
- 부울 프로퍼티는 어떻게 설정하는가?
Setting Method를 사용하는 방법이 제일 간단하다. 예를 들어, "isOn"이란 인스턴스 변수에 Boolean을 저장하는 Switch가 있다고 치자. Setting Method는 다음과 같다:
Switch>>on: aBoolean
isOn := aBoolean
이 접근법에 두 가지 문제가 있다. 첫째, 스위치의 상태에 대한 표현을 고객들에게 노출시킨다. 이는 하나의 객체에서 표현을 변경 시 다른 다수의 객체에 반영해야 하는 상황에 직면하게 만들었다. 두 번째 문제는 "누가 스위치를 켜는가?"라는 간단한 질문에도 답하기 어렵다는 점이다.
두 상태에 구별된 메서드를 생성 시 본래 의도보다 하나 더 많은 메서드를 생성해야 한다. 하지만 결과적 코드에서 전달도가 향상되기 때문에 메서드의 비용은 그만한 가치를 한다.
두 상태의 이름을 메서드의 이름으로서 사용하는 것은 꽤 솔깃한 방법이다 (이번 예제에서 Switch>>on과 Switch>>off). 하지만 Query Method에서와 마찬가지로, 객체에서 정보를 얻으려는 건지 아니면 해야 할 일을 알려주는 건지 혼동될 가능성이 있다. 따라서 선택자에 다른 단어를 추가하면 자연스러움은 덜할지라도 명확성의 증가만으로 그만한 가치가 있다.
- "be"로 시작하는 두 개의 메서드를 생성하라. 하나는 프로퍼티명을 갖고 있고 나머지 하나는 그렇지 않다. 고객이 현재 상태에 대해 알고 싶지 않다면 "toggle"을 추가하라.
예를 들어보겠다:
beVisible/beInvisible/toggleVisible
beDirty/beClean
Boolean Property Setting Methods(부울 프로퍼티 설정 메서드)를 "accessing"이라 불리는 메서드 프로토콜에 넣어라.
Role Suggesting Instance Variable Name (역할을 제시하는 인스턴스 변수명)
공통 상태(p.80)의 이름을 정할 필요가 있다.
- 인스턴스 변수를 어떻게 명명하는가?
어떤 변수건 그것을 전달하기 위해 중요한 두 가지 정보 조각이 있다:
- 목적이 무엇인가?
- 어떻게 사용되는가?
변수의 목적이나 역할은 독자들의 관심을 적절하게 조정하도록 도와주기 때문에 독자에게 중요하다. 일반적으로 코드를 읽을 때는 목적을 염두에 두고 있다. 변수의 역할을 이해했는데 당신의 목적과 무관할 경우 그 변수를 사용하는 코드 중 상관 없는 코드는 재빠르게 넘어갈 수 있다. 마찬가지로, 목적과 관련된 변수를 보면 그 변수를 찾아봄으로써 관련된 코드로 좁혀나갈 수 있다.
변수가 사용되는 방식과 메시지가 전송되는 방식을 "타입"이라 부른다. 스몰토크는 타입을 따로 선언하지 않지만 그렇다고 해서 중요하지 않다는 의미는 아니다. 변수로 전달된 메시지를 이해하면 어떤 객체를 값으로서 그 변수에 안전하게 위치시킬 수 있는지 알 수 있다. 객체의 대체는 엄정한 관리와 재사용에서 핵심이 된다.
서로 다른 변수들은 서로 다른 컨텍스트에 나타나므로, 그 이름을 통해 전달하는 내용은 각각 다르다. 인스턴스 변수에 대한 컨텍스트는 임시 변수에 대한 컨텍스트와 가장 비슷하다. 인스턴스 변수의 역할을 전달하는 유일한 방법은 그 이름을 통해서이다. Point에서 변수를 "x"와 "y" 대신 "t1"와 "t2"로 부른다면 어떤 것이 수평적이고 어떤 것이 수직적 컴포넌트인지 구별하기까지 훨씬 더 많은 내용을 읽어야 할 것이다. 변수를 역할에 따라 명명하면 그러한 정보를 직접적으로 제공한다.
반면 인스턴스 변수의 타입은 그것이 상주하는 코드에서 쉽게 발견할 수 있다. 변수가 어디서 사용되는지는 쉽게 발견할 수 있으며, 그 곳에서부터 어떤 메시지가 전송되는지 찾으면 된다. 또한 변수의 값을 설정하는 Creation Parameter Setting Methods(생성 파라미터 설정 메서드) 또는 Setting Methods에서도 힌트를 얻을 수 있겟다. 아래에서 "x"의 타입이 무엇인지 당신에게 묻는다면:
Point>>x: aNumber
x := aNumber
즉시 답할 수 있을 것이다. 명명을 단순하고 짧게, 그리고 읽기 쉽게 유지하기 위해선 변수의 타입은 이름에서 언급하지 않는 것이 좋다.
- 계산에서 인스턴스 변수가 하는 역할에 따라 명명하라. 변수가 Collection을 보유한다면 이름은 복수로 나타내라.
인스턴스 변수 "x"의 역할은 Point의 가로 원점을 포함하는 것이고, "y" 인스턴스 변수의 역할은 세로 원점을 보유하는 것이다.