SmalltalkBestPracticePatterns:5.2
- 5.2 클래스(classes)
클래스
해당 절은 각 주요 컬렉션 클래스의 사용을 선택하게 되는 상황을 제시한다.
컬렉션
- 일대다(one-to-many) 관계는 어떻게 표현하는가?
모든 프로그래밍 언어는 일대다 관계를 표현하기 위한 수단을 제공한다. FORTRAN에서는 배열(array)이 첫 번째 데이터 구조였다.
컴퓨터 과학은 일대다 관계의 표현을 독점적으로 만들어왔다. ACM 저널에는 대체 몇 천개의 다양한 트리, 리스트, 테이블이 숨어 있을까?
이러한 중요한 주제는 모든 프로그래밍 언어에서 엄청난 양의 관용구을 발견했다. 필자는 더 이상 C 언어를 사용하지 않지만 아래를 보면:
for (i = 0; i < n; i++) ...
즉시 배열을 반복하고 있음을 알아차릴 수 있다.
스몰토크의 초기 개발에서 가장 눈부신 성과 중 하나는 일대다 관계의 다양한 표현 방법들에 대해 통일화된 프로토콜을 표현하는 것이었다. 더 이상 모든 고객 코드가 요소를 반복하는 방식을 직접 인코딩하지 않아도 되었다. 반복(Iteration)은 관계를 표현하는 객체의 책임을 의미한다. 이는 개발 내내 엄청난 힘을 야기한다. "OrderedCollection"를 "Set" 변경함으로써 선형 리스트를 해시 테이블로 변경시키는 동시에 다른 어떤 코드도 영향을 받지 않을 것이라 확신할 수 있다.
컬렉션 클래스의 단점 또한 강력한 힘에 있다. 몇 개의 단어만으로 Collections를 관여시키는 코드를 작성할 수 있기 때문에 그러한 코드를 이해하는 기회를 갖기도 전에 단어들의 특별한 의미에서부터 시작해야 한다. 하지만 어휘(vocabulary)를 이해하고 나면 "스몰토크에서 이 작업은 정말 쉬울 텐데"라고 아쉬워하는 기색 없이 Collections를 지원하지 않는 언어에서 코드를 작성할 일은 절대 없을 것이다.
컬렉션 클래스 사용의 대안책도 있다. 어떤 클래스도 색인 가능(indexable)하게 만들 수 있는데, 즉 명명된(named) 변수, 그리고 at: 과 at:put: 을 통해 접근하는 변수들을 가질 수 있다. 이러한 기능을 이용해 어떠한 클래스도 일반 객체인 동시 컬렉션으로서 행위하도록 만들 수 있다. 공간이 극히 부족할 때는 매우 유용하겠지만 Collection 코드를 복사할 만큼의 가치는 없다.
- Collection을 이용하라.
Library는 Books의 Collection을 유지할 수 있다.
동적으로 크기를 변경하는 Collections(p.116)에 대해 OrderedCollection을 이용하라. 고정된 크기의 Collections에는 Array(p.133)을 이용하라. 요소의 유일성을 보장하기 위해 Set(p.119)를 이용하라. 한 가지 종류의 객체를 다른 종류의 객체로 매핑하려면 Dictionary(p.128)를 이용하라. 요소들을 정렬하려면 Temporarily Sorted Collection(p.155)를 이용하라. SortedCollection(p.131)을 이용해 요소들을 계산된 순서로 유지하라.
OrderedCollection
동적으로 크기가 조정된 Collection(p.115)이 필요하다.
- 생성 시 크기를 결정할 수 없는 Collections은 어떻게 코딩하는가?
프로그램이 사용자를 번거롭게 만드는 일들 중 다수는 유연성의 필요성으로부터 발생한다. 초기 메모리 관리에는 데이터 크기의 유연성이 결여되어 있었다. "99개 파일까지 가질 수 있습니다" 또는 "함수마다 256개의 행을 초과해선 안 됩니다"라는 글을 읽을 때마다 그 정도로 많은 항목을 처리하는 메모리를 사전 할당해주는(preallocate) 소프트웨어 엔지니어는 없는지 상상하곤 한다. "99" 또는 "256"과 같은 임의적인 제한을 두는 것은 보통 엔지니어가 그 이상의 크기가 필요할 것이라고 예상하지 못했기 때문이고, 언젠간 사용자에게 장애물이 된다 (때로는 몇년이 지나고 나서).
스몰토크에서는 데이터 크기의 임의적 제한을 핑계댈 수 없다. 그러한 제한을 제거하려할 때 두 가지 요인들이 당신에게 유리하게 작동할 것이다. 첫째, Collection 클래스들은 일대다 관계를 표현하는 것과 관련해 마음을 바꾸도록 엄청난 영향력을 제공할 것이다. 둘째, garbage collector는 데이터 구조가 크기가 변경되면서 참조를 유지해야 하는 힘들고 단조로운 작업으로부터 해방시켜준다.
이러한 유연성에는 비용이 들기 마련이다. 가장 일반적이자 동적으로 크기가 조정되는 Collection, 즉 OrderedCollection의 구현부는 처음부터 절확히 필요로 하는 양보다 더 많은 메모리를 할당하므로 증가량의 일부는 비용이 거의 들지 않는다. 구현부는 또한 간접적인 요소 접근을 이용한다. 따라서 일부 Collection 중심적인(Collection-intensive) 코드에서는 이것이 병목현상이 되는 것을 목격한 바 있다. 하지만 프로그램이 작동하도록 노력하면서도 그러한 문제를 염려하지 않아도 된다. 후에 이 문제를 다룰 시간은 얼마든지 있다.
- OrderedCollection을 동적으로 크기가 조정된 기본 Collection으로서 이용하라.
OrderedCollection의 사용 예제를 들어보겠다:
Class: Document
superclass: Object
instance variables: paragraphs
Document는 여기서 느긋한 초기화로 정의된 Paragraphs의 OrderedCollection을 저장한다:
Document>>paragraphs
paragraphs isNil ifTrue: [paragraphs :=
OrderedCollection new].
^paragraphs
우리는 여기서 새로운 Paragraphs를 동적으로 추가할 수 있어야 하므로, 크기를 동적으로 변경할 수 있는 OrderedCollection의 기능이 필요하다:
Document>>addParagraph: aParagraph
self paragraphs add: aParagraph
Document의 타입지정(typesetting)은 각 Paragraph를 차례로 타입지정함으로써 구현된다. Paragraphs의 순서는 중요하므로 OrderedCollection에 저장하는 것이 옳다.
Document>>typesetOn: aPrinter
self paragraphs do: [:each | each typesetOn: aPrinter]
모든 요소가 한 번만 나타나도록 확보해야 하는 경우 Set(p.119)로 변경하라. 동일한 요소의 long run과 함께 컬렉션을 압축적으로 표현하고 싶다면 RunArray(p.118)로 변경하라. 동적인 크기조정이 필요하지 않다면 Array(p.133)로 변경하라.
RunArray
OrderedCollection(p.116)을 사용하고 있다.
- 한 행에 동일한 요소를 여러 번 포함하는 Array 또는 OrderedCollection을 어떻게 압축적으로(compactly) 코딩하는가?
가장 간단한 방법은 OrderedCollection을 사용하는 방법으로, 개발 초기에는 올바른 해답이었을 것이다. 현실적인 크기의 데이터 집합으로 작업하기 시작하면 당신이 감당할 수 있는 것보다 더 많은 메모리를 사용하고 있음을 발견할 것이다.
이의 전형적인 예가 바로 텍스트 편집이다. 개념적으로 에디터 내 각 문자(character)는 고유의 글꼴, 크기, 꾸미기(decoration)가 있어야 한다. 이것을 분명하게 표현하는 것은 문자에 필적하는 Style 객체의 OrderedCollection이다. 짧은 텍스트에는 작동할지도 모른다. 하지만 책 전체를 다룬다면 OrderedCollection 내 슬롯에 추가로 4 바이트, Style 헤더에 12바이트, Style 내 변수에 대해 12 바이트, 그리고 Font, Size, Decoration 객체들에 필요한 바이트를 모두 더하면 엄청나게 커진다.
일반 텍스트를 보면 대부분의 정보가 중복됨을 발견할 것이다. 특정 스타일로 된 문자가 50개, 다른 스타일의 문자 200개, 또 다른 스타일이 90개 등등으로 추가될 것이다. RunArray는 이를 "50 of style 1, 200 of style 2, 90 of style 3(스타일 1에 50, 스타일 2에 200, 스타일 3에 90)"으로 저장한다. 각각의 개수-객체 쌍을 "run"이라 부른다.
OrderedCollection에 이러한 정보를 저장한 경우 340개의 요소가 필요할 것이다. RunArray 는 세 개의 run만 필요하다 (각각 두 개의 참조).
저장 효율성에는 비용이 든다. 첫 번째, 정말로 각 요소마다 다른 객체를 갖고 있다면 RunArray 는 Array 또는 OrderedCollection 에 비해 두 배의 저장 공간을 소요할 것이다. 둘째, RunArray 가 끝날 때 요소로 접근하는 데에 필요한 시간은 run 횟수와 비례한다. VisualWorks 구현부는 마지막으로 불러온 요소의 위치를 캐싱(caching)함으로써 이러한 문제를 어느 정도 피한다.
- RunArray를 사용해 동일한 요소의 long run 을 압축하라.
RunArray의 일반적인 예로 강조된 텍스트의 저장을 들 수 있다. 아래와 같은 텍스트 조각에서:
문자 자체는 14 element String에 저장될 수 있다. 강조부분은 그에 상응하는 14 element 배열에 저장 가능하다:
#(plain plain plain plain plain bold bold bold bold plain plain
plain plain plain)
이는 문자당 추가로 4 바이트, 또는 56 바이트를 소요한다. 하지만 주위에 동일한 요소가 너무나 많기 때문에 RunArray는 공간을 절약할 수 있다. 이를 저장(storing)으로 생각할 수 있다:
5—#plain, 4—#bold, 5—#plain
위는 6x4 바이트, 즉 24 바이트만 차지한다.
Array 또는 OrderedCollection의 인스펙터에서 "self"를 클릭할 때 RunArray가 필요함을 알고 있으며, 동일한 요소가 계속해서 인쇄되는 것을 목격한다.
RunArray의 장점은 고객 코드가 모두 동일한 메시지를 지원하므로 그것이 Array, OrderedCollection, RunArray 셋 중에 어느 것을 갖고 있는지 알지 못한다는 점이다. 그저 가장 효과적인 것을 이용해 코딩한 다음 추후에 큰 비용을 들이지 않고 생각을 바꾸면 된다.
VisualWorks 2는 RunArray를 구현한다. VisualAge와 VisualSmalltalk는 아직 구현하지 않는다(책을 쓰는 시점을 기준으로).
Set
유일한 요소들로 된 Collection(p.115)가 필요하다. 중복된 내용이 없는 Collecting Parameter(p.75)가 필요할 수도 있다. OrderedCollection(p.116)를 사용해 왔을 수도 있다.
- 유일한 요소들로 구성된 Collection을 어떻게 코딩하는가?
Accounts의 Collection가 있는데 모든 Owners에게 statement를 전송하길 원한다고 가정해보자. 어디에도 Owners의 명시적인 Collection은 없다. 단지 각 Owner에게 단일 statement만 전송하고자 한다. 각 Owner마다 Accounts의 수가 다를 수도 있다.
어떻게 하면 각 Owner에게 한 번씩만 전송할 수 있을까? 아래와 같이 솔직한 코드는 올바르게 작동하지 않는다:
owners
| results |
results := OrderedCollection new.
self accounts do: [:each | results add: each owner].
^results
다수의 Owner 복사본을 얻을지도 모른다. 이 문제는, 추가(add)를 하기 전에 소유주(owner)가 결과에 존재했는지 확인함으로써 해결할 수 있겠다:
owners
| results |
results := OrderedCollection new.
self accounts do:
[:each || owner |
owner := each owner.
(results includes: owner) ifFalse: [results add:
owner]].
^results
Sets는 anObject가 이미 Set의 일부일 경우 "add:anObject"를 무시함으로써 문제를 해결해준다. OrderedCollection 대신 Set를 이용하면 문제를 수정한다.
owners
| results |
results := Set new.
self accounts do: [:each | results add: each owner].
^results
이러한 차이를 바라보는 또 다른 방법은, 작업 공간(workspace)에서 두 개의 Collections를 모두 시도해보는 방법이다:
|o|
o := OrderedCollection new.
'abc' String을 Collection에 넣으면 한 번만 발생한다:
o add: 'abc'.
o occurrencesOf: 'abc' => 1
다시 넣으면 두 번 발생한다:
o add: 'abc'.
o occurrencesOf: 'abc' => 2
넣었던 것을 한 번 꺼내면 한 번만 발생한다:
o remove: 'abc'.
o occurrencesOf: 'abc" => 1
Sets는 다른 행위를 표시한다.
|s|
s := Set new.
OrderedCollection과 마찬가지로 'abc'를 한 번만 넣으면 한 번만 발생한다:
s add: 'abc'.
s occurrencesOf: 'abc" => 1
하지만 다시 넣어도 여전히 한 번만 발생한다:
s add: 'abc'.
s occurrencesOf: 'abc" => 1
하나를 꺼내면 사라진다:
s remove: 'abc'.
s occurrencesOf: 'abc" => 0
(그건 그렇고, 필자는 어떤 새로운 코드를 이해하려고 할 때마다 작업 공간(workspace)을 꺼내 위와 같이 이것저것 조작해보기 시작한다. 인스턴스를 골라서 그곳으로 메시지들을 전송하는 방법보다 새로운 객체를 이해하기 좋은 방법은 없다. 무슨 일이 일어나고 있는지 이해가 된다고 판단되면 어떻게 반응할 것인지 예측해보라. 전혀 모르겠다면 메시지를 전송하여 무엇이 돌아오는지 확인하라.)
중복내용을 제거하는 데에는 비용이 든다. OrderedCollection이 고수하는 추가 및 제거 순서는 Sets에서 이용할 수 없다. 또한 OrderedCollection은 at: 또는 at:put: 과 같은 인덱스된(indexed) 메시지에 응답하는데, Sets는 그렇지 않다.
- Set를 이용하라.
중복내용을 원치 않을 시 Sets를 이용하라. 모든 클럽 구성원들의 Collection을 얻고 싶다면 아래와 같이 쓰면 된다:
clubMembers
| results |
results := OrderedCollection new.
self clubs do: [:each | results addAll: each members].
^results
한 학생이 하나 이상의 클럽에 가입할 수 있는 경우, 그 학생은 결과에 한 번 이상 표시될 것이다. 이를 피하기 위해 아래와 같이 작성한다:
clubMembers
| results |
results := Set new.
self clubs do: [:each | results addAll: each members].
^results
Sets는 어떤 Collection 클래스보다 비호환성(incompatibility) 문제들을 가장 많이 야기하였을것이다. 인덱스 가능한(indexable) Collection을 기대하는 리스트 패인으로 영원히 Set를 전달하고 있다. 이는 Array 또는 SortedCollection을 생성함으로써 쉽게 해결할 수 있는 문제다.
memberList: aPane
aPane contents: self clubMembers
위의 코드는 아래가 된다:
memberList: aPane
aPane contents: self clubMembers
asSortedCollection
Sets는 중요한 통신기능을 수행한다. 이를 통해 당신은 독자들에게 "해당 Collection 내에 중복내용이 있을 것으로 예상되지만 추가적 처리는 원하지 않습니다,"라고 말해준다. 단, 자신의 의도에 부합할 때에만 사용하도록 하라. 그래야만 다른 사람들도 당신이 의도한 대로 읽을 것이다.
Set를 사용하는 또 다른 이유는 includes: 의 구현부가 OrderedCollection (큰 Collections용) 내의 구현부보다 훨씬 빠르기 때문이다. 일을 하면서 이를 실행한 경험은 손에 꼽지만 성능을 조정할 때 사용하면 좋은 기법이다.
Set 요소에 대해 Equality Method(p.124)를 구현하는 경우, Hasing Method(p.126) 또한 구현해야 한다. 인덱스 가능한 Collection을 원하는 고객을 위해 Array (p.133) 또는 Temporarily Sorted Collection(p.155)를 구현할 필요가 있다.
Equality Method (동등성 메서드)
- 새로운 객체들에 대한 동등성(equality)을 어떻게 코딩하는가?
"=" 메시지는 표준 VisualWorks 2 이미지에서 1363회 전송된다. 그리고 57회 구현된다. 두 객체는 동일한 객체일 때, 그리고 동일한 객체일 때에만 서로 동등하다.
Object>>= anObject
^self == anObject
동등성이 57회만 재정의될 경우 위의 정의부는 대다수 클래스에 충분할 것이다.
동등성을 구현하는 가장 중요한 이유는 당신이 객체들을 Collection에 넣으려 할 때 inclusion를 검사하거나, 요소를 제거하거나, 동일한 인스턴스를 가질 필요 없이 Sets에서 중복 내용의 제거가 가능하도록 만들고 싶기 때문이다.
예를 들어, 도서관 프로그램을 작업하고 있다고 치자. 두 권의 책 저자와 제목이 동일하면 두 책은 동일하다:
Book>>= aBook
^self author = aBook author & (self title = aBook title)
Library는 Books의 OrderedCollection을 관리한다. 동등성을 정의했기 때문에 필자는 저자명과 제목을 취하여 해당하는 Book을 검색하는 메서드를 작성할 수 있다:
Library>>hasAuthor: authorString title: titleString
| book |
book := Book
author: authorString
title: titleString.
^self books includes: book
동등성을 구현하는 또 다른 중요한 이유는, 당신의 객체들이 동등성을 구현하는 다른 객체들과 상호작용을 해야하기 때문이다. 예를 들어, 새로운 종류의 Number를 구현하고 있다면 모든 Numbers는 동등성을 정의하므로 필자도 동등성을 정의할 것이다.
동등성 메서드에서 클래스 시험의 수가 꽤 많음을 목격한다. 예를 들어, 아래를 보면:
Book>>= aBook
(aBook isMemberOf: self class) ifFalse: [^false].
^self author = aBook author & (self title = aBook title)
필자는 위의 코드를 "aBook이 만일 Book이 아니라면 필자(I)는 그것(it)과 동등해질 수 없다,"로 해석된다. 혹은 저자와 제목이 aBook의 것과 동일하다면 필자(I)도 그것(it)과 동등하다. 클래스 시험은 일반적으로는 나쁜 것이지만 (Choosing Message 참고), 여기서는 유용하게 사용될 수 있다. Library에 Videos를 추가하길 원한다면 어떨까? Videos는 "author"라는 메시지가 없기 때문에 어떠한 보호장치도 없이 Book과 Video를 비교할 경우 오류가 야기될 것이다.
- Set에 객체들을 넣은 후 Dictionary 키로서 이용하거나 또는 동등성을 정의하는 다른 객체와 함께 이용하는 경우, "="라 불리는 메서드를 정의하라. "="의 구현부를 보호하여 호환 가능한 클래스의 객체들만 동등성 시험을 받도록 하라.
Numbers나 Points와 같이 Value와 같은 객체들은 가장 확실한 Equality Methods의 후보들이다. Two Points의 좌표가 동일하다면 둘은 동일하다.
Point>>= aPoint
(aPoint isMemberOf: self class) ifFalse: [^false].
^x = aPoint x & (y = aPoint y)
복잡한 멀티클래스 동등성 시험을 위해 Double Dispatch(p.55)가 필요할 수 있다. 시험은 객체에 대한 하나 또는 그 이상 컴포넌트에 대한 Simple Delegation(p.65)과 관련해서 주로 실행된다.
Hashing Method (해싱 메소드)
Equality Method(p.124)를 작성했다. 새로운 객체를 Set(p.119)로 넣을 수 있다. 새로운 객체를 Dictionary (p.128) 내에서 키(key)로서 사용할 수 있다.
- 새로운 객체들이 hashed Collections와 올바르게 작동하도록 보장하는 방법은?
"="를 오버라이드할 수 있음을 이해하고 나면 이제 못할 것이 없다. 동등성에 대해 자신만의 정의를 가진 새 객체들은 자동적으로 나머지 스몰토크와 어울리기를 시도한다.
Collection>includes:는 동등성을 사용한다:
Collection>>includes: anObject
self do: [:each | each = anObject ifTrue: [^true]].
^false
따라서 OrderedCollection, 그리고 새로 동일해진 객체들과 함께 시작하면 모든 것은 제대로 작동한다. 그러다 Set가 필요하다는 결정을 내린다. 갑자기 버그가 생기기 시작한다. Set를 확인하니 그 안에 동일한 것으로 알고 있는 두 개의 객체가 있다. Sets는 이러한 문제를 피하는 것이 아니었던가?
문제는 Set>>includes: 가 "="를 전송하는 데에 그치지 않고 "hash" 또한 전송한다는 데에 있었다. 두 개의 객체가 동일하지만 동일한 해시 값을 리턴하지 않을 경우, Set는 그들이 동일하다는 사실을 놓칠 가능성이 있다.
- "="를 오버라이드하고 hashed Collection의 객체를 사용하는 경우, "hash"를 오버라이드해야 동일한 두 객체가 동일한 해시 값을 리턴한다.
이 규칙이 추상적으로 보일지도 모른다. 하지만 사실상 이러한 제약은 간단하게 충족시킬 수 있다. Equality Method는 보통 동일한 결과를 리턴하는 일부 메시지 집합에 의존한다:
Book>>= aBook
^self title = aBook title & (self author = aBook author)
"hash"의 구현부에서 처음으로 자른 부분은 Equality Method가 의존하는 모든 메시지들의 해시 값을 얻어 bit-wise exclusive-or과 함께 넣기 위함이다.
Book>>hash
^self title hash bitXor: self author hash
모든 서브컴포넌트들은 Hashing Method 패턴을 따르기 때문에 메시지가 Strings, Numbers, Arrays 등등 어떤 것을 리턴하든 작동한다.
Hashing은 종종 Simple Delegation(p.65)와 관련해 구현된다.
Dictionary (사전)
연속된 정수가 아닌 다른 무언가로 인덱스된 Collection(p.115)이 필요하다.
- 한 종류의 객체를 다른 종류의 객체로 어떻게 매핑하는가?
Arrays와 Orderedcollections은 정수를 객체로 매핑한다. 정수는 1과 컬렉션 크기 사이에 위치해야 한다. Dictionaries는 좀 더 유연한 맵(map)이다. 그들의 키(key)는 순차적 정수 외에 어떠한 객체든 될 수 있다.
Dictionary는 어떤 대상의 이름을 대상으로 매핑하는 데에 일반적으로 사용된다:
colors
| result |
result := Dictionary new.
result
at: 'red'
put: (Color red: 1).
result
at: 'gray'
put: (Color brightness: 0.5).
^result
이러한 문제를 해결하는 한 가지 방법으로, Color에 "name(이름)" 인스턴스 변수를 추가하는 방법이 있다. 사실 이는 "객체 지향적" 해결책으로 보인다. 이 방법이 효과가 없는 이유가 무얼까?
시스템의 각기 다른 부분들은 동일한 색상에 대해 다른 이름을 원할 것이다. 어떤 곳에선 "red"로 불리는 색상이 다른 곳에선 "warning"이라 불리기도 한다. Color에 "colorName"이나 "purposeName" 인스턴스 변수를 추가할 수 있을 것이라 가정하겠다. 이것은 어디에서 끝이 날까? 색상의 새로운 애플리케이션은 모두 새로운 인스턴스 변수를 추가해야 할 것이다. 유일한 이름을 필요로 하는 각 객체에서 Dictionary를 사용하는 편이 훨씬 수월하다.
이 문제를 해결하는 또 다른 방법은 Color를 둘러싸고 그것에 이름을 제공하는 객체를 생성하는 것이다.
NamedColor
superclass: Object
instance variables: name color
필자가 이러한 객체를 구현할 때마다 그 객체는 이름과 둘러싼(wrapped) 객체에 대한 Getting Methods 이상의 책임은 지지는 않았다. 이러한 작업만을 위해 새로운 객체를 생성하는 것은 그만한 의미가 없다. Dictionary를 이용해 색상과 이름을 관계시키는 경우 소수의 행으로 가능하며 이해도 쉽다.
- Dictionary를 이용하라.
Dictionaries은 어떤 대상에 이름을 부여하는 데에 잘 사용된다. 예를 들어, 나에게 위젯이 하나 있는데 각기 다른 색상마다 다른 의미를 가진다고 가정하면, 이를 Dictionary에 저장할 수 있다:
Widget>>defaultColors
| results |
results := Dictionary new.
results
at: 'foreground'
put: Color black.
results
at: 'background'
put: Color mauve.
^results
이따금씩 시간에 쫓기는 프로그래머는 Dictionaries를 저렴한 데이터 구조로 사용한다. 두 개 또는 그 이상의 메서드가 정확히 동일하게 고정된 키(key) 집합을 사용할 것이므로 이와 같은 코드는 언제든 포착할 수 있다.
vitalInformation
| result |
result := Dictionary new.
result
at: 'weight'
put: 190.
result
at: 'hair'
put: 'blond'.
^result
checkOut: aPerson
| info |
info := aPerson vitalInformation.
(info at: 'weight') < 280 & ((info at: 'hair') = 'black')
ifTrue: ...
각 인덱스가 다른 대상을 의미하는 고정된 크기의 Arrays에서도 이와 같은 일을 실행했다.
vitalInformation
^Array
with: 190
with: 'blond'
checkOut: aPerson
| info |
info := aPerson vitalInformation.
(info at: 1) < 280 & ((info at: 2) = 'black') ifTrue: ...
이것은 당신의 프로그램이 "여기 새 객체가 있습니다,"라고 말하는 것이다. 새 객체는 컬렉션 내 요소마다 하나의 인스턴스 변수를 가질 것이다.
Dictionary 키에 대해 Equality Method(p.124)를 구현할 경우 Hashing Method(p.126) 또한 구현해야 한다.
SortedCollection
요소의 일부 속성에 따라 정렬되어야 하는 Collection(p.115)을 갖고 있다.
- 컬렉션은 어떻게 정렬하는가?
필자가 대학에 다닐 때 정렬은 매우 중요한 일이었다. 필자는 당시 Knuth를 공부하면서 기발한 정렬 방식들을 학습했다. 정렬 알고리즘(sorting algorithm)에 얼마나 많은 계산 시간이 투입되고, 조그만 개선으로도 얼마나 엄청난 영향을 미칠 수 있는지에 대한 이야기를 들었다. 스몰토크 프로그래밍을 시작한 지 11년이 지났는데도 아직 정렬 알고리즘을 작성하지 못했을 때 필자가 겪은 스트레스를 상상해보라. 모든 뇌세포를 낭비한 것이다!
물론 전적으로 낭비되는 것은 아니다. 성공적인 프로그래머가 되려면 내부에서 어떤 일이 일어나고 있는지를 알고 있어야 한다. 단지 스몰토크의 컬렉션 클래스들은 정렬이 어떻게 구현되는지에 대한 인식을 드물고 화려한 이벤트로 바꾸어 놓을 뿐이다.
요소가 추가되어 (OrderedCollection) 비정렬되는(unorder)ㅡ하지만 여전히 효율적인(Set)ㅡ시기에 따라 순서가 결정되는 컬렉션이 있는 것처럼, 정렬은 특별한 종류의 컬렉션 SortedCollection의 또 다른 속성일 뿐이다.
- SortedCollection을 이용하라. "<=" 이외의 기준으로 정렬을 원할 경우 그 정렬 블록(sort block)을 설정하라.
어떤 컬렉션이든 그곳으로 "asSortedCollection"을 전송함으로써 정렬할 수 있다. 기본적으로 SortedCollections는 요소의 비교 시 "<"를 사용한다. Numbers, Strings, Date와 Time, 그리고 기타 몇 가지 객체들은 이미 "<"를 정의한다. 이들을 "SortedCollection new"의 결과에 넣을 생각이라면 당신의 객체에 대해 "<"를 정의해야 할 것이다.
Sortedcollections는 내부적으로 두 개의 argument 블록을 사용하여 요소들을 비교한다. 이러한 정렬 블록은 자신이 원하는 대로 설정할 수 있다. 자녀(childre)를 연령별로 정렬해보자:
childrenByAge
^self children asSortedCollection: [:a :b | a age < b age]
연령으로 정렬하는 이유는 연령은 Numbers에 해당하고 숫자는 "<"를 정의하기 때문이다. 이름을 손쉽게 알파벳순으로 정렬할 수도 있다:
children byName
^self children asSortedCollection: [:a :b | a name < b
name]
이미 SortedCollection을 갖고 있는데 그 순서를 변경하고자 할 경우, 순서를 변경하기 위해 "sortBlock: aBlock"을 전송하면 된다.
self childrenByAge sortBlock: [:a :b | a income > b income]
한 가지 주의해야 할 위험한 성능은, SortedCollection을 빌드할 경우 한 번에 하나의 요소만 빌드해야 한다는 점이다. SortedCollections는 요소가 추가되거나 제거될 때마다 재정렬을 실행한다. SortedCollection의 요소를 한 번에 빌드해야 하는 경우 임시로 OrderedCollection을 사용한 후 완료되면 SortedCollection으로 바꾸는 방법을 고려해보라.
Array
요소의 수가 고정된 Collection(p.115)이 필요하다. OrderedCollection(p.116)을 사용해왔을 수도 있다.
- 요소의 수가 고정된 컬렉션은 어떻게 코딩하는가?
OrderedCollection은 유연성의 장점을 찬미한다. 얼마나 많은 요소가 필요한가? 왜 지금 당장 결정해야 하는가? 그냥 OrderedCollection을 사용해 모든 임의적 제한을 피하라.
컬렉션을 생성할 때 얼마나 커질 것인지 정확히 알고 있다면 어떨까? OrderedCollection을 사용하여 요소를 추가할 수는 있지만 코드는 컬렉션의 크기가 고정되었다는 정보를 잃었을 것이다. 당신이 "여기 컬렉션이 있다. 컬렉션의 크기는 이러이러하다. 이 크기는 절대 변하지 않을 것이다,"라고 말해준다면 코드는 정보를 훨씬 더 잘 전달할 것이다.
고정된 크기의 컬렉션을 사용하는 두 번째 이유는 효율성 때문이다. OrderedCollection의 유연성은 추가 메시지가 요소로 접근해야 하므로 비용이 든다. 대부분의 구현부는 OrderedCollections를 두 개의 객체로 나누기 때문에 추가적 공간 오버헤드(space overhead)가 존재한다.
보통은 효율성을 그다지 신경 쓰지 않겠지만 한 때 필자는 한 번의 성능 조정, 즉 OrderedCollection을 Arrays로 대체함으로써 속도를 40%까지 개선시킨 경험이 있다. 물론 코드는 스몰토크가 아니라 FORTRAN처럼 보였지만 성능을 조정할 때는 너무 큰 욕심을 부리기보다는 조금 더 빠른 속도만 목표로 한다.
- Array를 이용하라. "new: anInteger"를 이용해 당신이 생각하기에 필요한 요소의 수만큼 공간을 갖고 있도록 생성하라.
Ward Cunningham이 변수 크기의 배열을 시뮬레이션하는 기법을 가르쳐준 적이 있다. Array에 추가하고 그로부터 제거하는 것보다 훨씬 더 많이 접근하는 경우에만 유효하다. 아래와 같은 코드를:
children := OrderedCollection new
아래와 같이 변경한다:
children := Array new "or even #()"
요소를 추가해야 하는 경우 컬렉션에 추가하는 대신:
children add: anObject
추가된 요소가 있는 새 Array를 생성하여 변수로 할당할 수 있다:
children := children copyWith: anObject
이와 비슷하게, 요소를 제거하기 위해선:
children remove: anObject
요소가 빠진 새 Array를 생성한다:
children := children copyWithout: anObject
ByteArray(p.135)를 이용해 1...255 범위의 SmallIntegers를 저장하라. 순차적 Numbers의 Array를 표현하기 위해 Interval(p.137)를 이용하라. 반복된 요소의 long run을 가진 Arrays를 압축적으로 저장하기 위해선 RunArray(p.118)를 이용하라.
ByteArray
작은 수로 된 Array(p.133)를 표현해야 한다.
- 0..255 또는 128..127 범위의 수들로 이루어진 Array를 어떻게 코딩하는가?
소프트웨어 공학은 자원이 부족한 환경에서 생겼다. 우리가 원하는 것을 실행하기에 충분한 싸이클이나 메모리가 있었던 적이 한 번도 없다. 물론 과거만큼은 아니겠지만 애플리케이션의 수요 증가는 이용 가능한 자원에 항상 영향을 미칠 것이다.
이용 가능한 메모리의 결여에 대한 반응 중에서 가장 창의적인 해답은 소프트웨어 공학이 8-bit 바이트의 사용 방법을 발견했을 때였다. EBCDIC와 ASCII 코드는 인쇄 가능한 문자를 바이트로서 코딩한다. P-Code와 스몰토크 바이트 코드 명령어 집합은 가상 머신 명령어를 바이트로서 코딩한다. 4 바이트 혹은 8바이트의 결합(concatenation)은 메모리 주소를 형성할 수 있다.
반면, 스몰토크 프로그램이 최대한 유연하기 위해서는 가능한 한 표현(representation) 결정으로부터 보호되길 원할 것이다. 하지만 한정된 환경에서 코딩이 가능하려면 공간 효율성에 관한 고려사항도 어느 정도 가능해야 한다.
정보가 8-bit 바이트로 저장됨을 정말로 알리고 싶을 때가 종종 있다. 그 때 좀 더 일반적인 표현을 사용하면 오해를 불러일으키기 쉽다. 자신의 의도를 가장 직접적으로 전달하는 표현을 사용하길 원할 것이다.
컬렉션 클래스들은 숨겨진 표현의 유연성, 정보를 바이트로 인코딩하는 공간적 효율성을 확보하는 훌륭한 수단을 제공한다. 바이트만 저장하는 표현을 선택하면 해당 컬렉션에 포함된 객체는 0..255 범위의 SmallIntegers만 가능하다는 사실을 효과적으로 전달한다.
- ByteArray를 사용하라.
ByteArrays는 이미지의 픽셀이나 벡터의 부동 소수점 수와 같이 동질한 정보를 저장하는 데에 사용된다.
Class: Vector
superclass: Object
instance variables: numbers
특정 크기로 된 Vector를 생성할 때에는 이것이 다수의 Floats를 저장하도록 충분한 바이트를 할당한다:
Vector class>>new: anInteger
^self new setNumbers: (ByteArray new: anInteger * 4)
Vector>>setNumbers: aByteArray
numbers := aByteArray
Vector에서 요소를 설정하면 Float로부터 바이트를 취하여 ByteArray에 집어 넣는다:
Vector>>at: anInteger put: aFloat
1 to: 4 do:
[:each |
numbers
at: anInteger * 4 + each + 1
put: (aFloat basicAt: each)]
SmallIntegers가 ByteArray에 있을 때에는 C 또는 어셈블리 언어에서 표현할 법한 모습으로 표현된다ㅡ간단한 8-bit 패턴으로 말이다. at:put:에 관한 코드는 SmallIntegers에 대한 스몰토크의 포맷에서 8-bit 패턴으로 변환시킨다. at: 에 관한 코드는 역변환을 실행한다.
수많은 정보를 바이트로 저장할 경우, "Array"로 된 몇 개의 참조를 "ByteArray"에 대한 참조로 변경하고 나면 속도가 급격히 증가하거나 메모리 사용량(memory footprint)이 급격히 감소함을 발견할 것이다. 32-bit 객체 참조로 이루어진 최신 스몰토크 시스템의 경우, 4배가 절약된다. 작은 Bytearrays가 많을 경우 절약할 수 있는 양이 적은데, 각 Bytearray(보통 8 또는 12 바이트)의 공간 오버해드는 그 내부에 저장된 정보에 비해 커질 것이기 때문이다.
Interval
- 숫자의 컬렉션을 어떻게 순차적으로 코딩하는가?
이따금씩 Array를 생성하고 그 요소들을 순차적 번호로 초기화하기 위한 코드를 작성하는 자신의 모습을 발견할 것이다.
| indexes |
indexes := Array new: 100.
1 to: indexes size do:
[:each |
indexes
at: each
put: each]
위는 "순차적 번호로 된 Array"를 전달하기에 그다지 직접적인 방식은 아니다. 또한 이와 같은 숫자의 Array를 표현하는 데에 시간과 공간이 소요된다.
특별히 이러한 목적을 위해 빌드되는 Interval이라 불리는 컬렉션 클래스가 있다. Interval을 사용 시에는 사실상 단점이 없다. 당신의 코드는 훨씬 쉽고 빠르게 읽힐 것이다.
- 시작, 정지, 선택적 단계 값으로 Interval을 이용하라. Shortcut Constructor Methods Number>>to: 와 to:by: 가 당신을 대신해 Intervals를 빌드해준다.
Intervals를 이용해 작성한 일부 코드는 처음에 혼동될 수 있다.
1 to: 20 do: [:each | ...]
위의 코드는 아래와 동등하다:
(1 to: 20) do: [:each | ...]
두 구문은 비슷해 보이지만 매우 다른 코드가 호출된다. 첫 번째 사례의 경우, to:do: 메시지가 Smallinteger 1으로 전송되고, 두 번째는 to: 메시지가 SmallInteger 1으로 전송된다. 이는 Interval을 리턴하고, 이렇게 리턴된 Interval은 후에 do: 메시지를 전송한다.
번호 순서의 설명을 Intervals로 해석할 수 있다:
설명 | Interval |
1부터 10까지 | 1 to: 10 |
0부터 50까지 짝수 | 0 to: 50 by: 2 |
99부터 3씩 하향으로 계수 | 99 to: 0 by: -3 |
열거 프로토콜에서 지원하지 않는 열거를 사용하길 원할 때가 있다. 예를 들어, Number>>to:do: 는 있지만 Number>>to:collect: 는 없다. 하지만 Interval를 사용할 수 있는 이유는 완전한(full fledged) 컬렉션이기 때문이다.
예를 들어, Array가 있는데 Associations의 Array, 원본 Array의 인덱스로 이루어진 키(key), 요소로 이루어진 값을 원한다면 아래와 같이 작성할 수 있다:
(1 to: anArray size) collect:
[:each |
Association
key: each
value: (anArray at: each)]