GnuSmalltalkUsersGuide:BasicChapter 06

From 흡혈양파의 번역工房
Jump to: navigation, search
제 6 장. 사용 지침서

Contents

사용 지침서

본 매뉴얼이 제공하는 내용

본 문서는 스몰토크 언어의 일반적인 지침을 소개하고, 그 중에서도 GNU Smalltalk의 구현에 대한 설명을 제공한다. 언어와 그 라이브러리의 모든 특징을 광범위하게 다루지는 않는다; 대신 스몰토크 초보자를 올바른 방향으로 인도하기 위해 수많은 중요한 아이디어와 기법들을 소개하고자 한다.


본 매뉴얼의 독자는...

본 매뉴얼은 독자가 컴퓨터 과학의 기본 내용을 습득한 상태이고 C와 같은 절차 언어에 관해 상당한 수준의 능력에 도달한 것으로 가정한다. 또한 프로그래밍과 관련된 기본적인 작업에 이미 익숙한 독자일 것으로 가정한다: 파일의 편집이나 이동 등의 작업.


시작하기

스몰토크 시작하기

GNU Smalltalk가 시스템에 설치되어 있다고 가정하면 스몰토크의 시작은 다음과 같이 간단하다:

localhost$ gst


이후 시스템이 스몰토크에서 로딩되고, 아래와 같이 시작 배너가 표시된다:

GNU Smalltalk ready

st>


이제 스몰토크를 시도해볼 준비가 된 것이다! 반면 종료할 준비가 되었을 때는 빈 행에 control-D 를 입력하면 스몰토크를 종료할 수 있을 것이다.


첫 인사하기

처음 연습은 스몰토크에게 'hello'라는 인사를 시키는 일이다. 다음 행에 아래를 입력하라 (printNl 에서 대문자 N과 소문자 L이다):

'Hello, world' printNl


이후 시스템은 'Hello, world'를 인쇄해줄 것이다. 두 번이 인쇄되는데, 첫 번째는 당신이 인쇄를 요청했기 때문이고 두 번째는 snipped가 'Hello, world' 문자열로 평가했기 때문이다.[1]


실제로 발생한 일

최우선(Front-line) 스몰토크 해석기는 '!' 문자 앞까지 모든 텍스트를 집합하여 실행한다. 따라서 실행된 실제 스몰토크 코드는 다음과 같다:

'Hello, world' printNl


이 코드는 두 가지 일을 한다. 첫째로는 'Hello, world' 문자를 포함하는 String 타입의 객체를 생성한다. 둘째는 printN1 으로 명명된 메시지를 객체로 전송한다.


객체가 메시지의 처리를 완료하면 코드는 끝이 나고 우리는 프롬프트를 다시 받는다. 사실상 문자열로 인쇄하라고 말하지 않았음에도 불구하고 인쇄되었음을 눈치 챌 것인데, 알고 보면 매우 고의적이다: 우리가 입력한 코드는 문자열의 인쇄에 대해서는 어떤 것도 알지 못한다. 코드는 문자열 객체를 얻는 방법을 알고, 그 객체로 메시지를 전송하는 방법을 알았을 뿐이다. 그것이 우리가 작성한 코드의 전부다.


그렇긴 하지만 문자열이 printNl 메시지를 수신할 때 어떤 일이 발생했는지를 한 번 살펴보자. 문자열 객체는 이후 문자열이 수신할 수 있는 메시지를 비롯해 어떤 코드를 실행할 것인지 열거한 테이블[2]로 들어간다. 그 테이블에는 printNl에 대한 엔트리가 존재하고 해당 코드를 실행하였음이 발견된다. 해당 코드는 그 문자를 차례로 살펴보면서 각 문자를 단말기로 인쇄하였다.[3]


여기서 핵심은 객체가 완전히 자립적(self-contained)이라는 사실이다; 객체만이 스스로를 어떻게 인쇄하는지 알고 있었다. 우리는 객체가 무언가를 인쇄하길 원할 경우 객체 자체로 인쇄를 요청한다.


수학문제 풀기

위와 유사한 코드 조각이 숫자를 인쇄한다:

1234 printNl


동일한 메시지를 사용하되 새로운 타입의 객체-정수(Integer 클래스로부터)-로 전송하였음을 주목하라. 정수가 인쇄되는 방식은 문자가 내부에서 인쇄되는 방식과 많이 다르지만, 우리는 메시지를 전송하고 있으므로 이를 인식할 필요가 없다. 우리가 printNl 에게 말하면 스스로 인쇄를 해준다.


객체의 사용자로서 우리는 특별한 메시지를 전송하면서도 기본적으로 객체의 내부 구조와 무관하게 동일한 유형의 행동을 기대한다 (예: 객체로 printNl을 전송하면 객체가 스스로를 인쇄하도록 만드는 것을 목격했다). 이후 여러 절에 걸쳐 광범위한 객체 타입을 살펴볼 것이다. 이들 모두는 동일한 방식으로-printNl을 이용해-인쇄 가능하다.


여백은 단어를 구분한다는 점만 제외하면 무시한다. 이 예제는 아래와 같은 모습일 것이다:


하지만 GNU Smalltalk는 가능한 한 스스로 하나의 행을 실행하고자 한다. 두 행에 코드를 작성하길 원한다면 아래와 같이 작성할 수 있겠다:

(1234
	printNl)


GNU Smalltalk가 해답의 인쇄를 제공하므로 이제부터 printNl을 생략할 것이다.


스스로를 인쇄하는 데 더해 정수로 다수의 메시지를 전송할 수 있다. 정수에 중요한 메시지 집합은 수학문제를 푸는 것들이다:

9+7


위의 (올바른!) 답은 16이다. 하지만 수학문제를 푸는 방식은 절차 언어와 크게 다르다.


스몰토크에서 수학

이러한 경우 객체 9(Integer)가 인자를 7로 (이 또한 Integer) 둔 + 메시지를 수신한 것이 실제로 발생한 일이다. 이후에 정수에 대한 + 메시지는 스몰토크로 하여금 새 객체 16을 생성하고 그것을 결과 객체로서 리턴하도록 야기하였다. 그리고 16 객체에 printNl 메시지가 주어지고, 단말기에 16을 인쇄하였다.


따라서 수학은 스몰토크에서는 특별 사례에 해당하지 않는다; 다른 모든 것과 정확히 동일하게 객체를 생성하고 객체에 메시지를 전송함으로써 완료할 수 있다. 스몰토크 초보자는 의아해할 수도 있지만 이러한 규칙성은 꽤 요긴한 것으로 나타났다: 몇 가지 양식만 숙달하면 언어의 모든 것은 앞뒤가 맞게 떨어질 것이다. 다음 장에 들어가기 전에 * (곱셈), - (뺄셈), /(나눗셈)을 수반하는 수학문제도 시도해보도록 하라. 아래 예가 시작에 도움이 될 것이다:

8 * (4 / 2)
8 - (4 + 1)
5 + 4)
2/3 + 7
2 + 3 * 4
2 + (3 * 4)


스몰토크 클래스 일부 사용하기

이번 장에는 생성하는 객체를 보유하는 장소가 필요한 예제들이 포함되어 있다. 그러한 장소는 필요 시 자동으로 생성된다; 당신이 저장한 객체를 모두 폐기하고 싶다면 문(statement) 맨 끝에 느낌표를 작성하라.


이제 객체를 몇 개 생성해보자.


스몰토크에서 배열

스몰토크에서 배열은 다른 언어에서의 배열과 유사하지만 그 구문을 처음 보면 생소할 수도 있다. 20개의 요소를 위한 공간이 있는 배열을 생성하기 위해서는 아래를 실행하라[4]:

x := Array new: 20


Array new: 20 은 배열을 생성한다; x := 부분은 x 이름을 객체와 연결한다. 그 외 다른 것을 x로 할당할 때까지 당신은 이 배열을 x 이름으로 참조할 수 있다. 배열의 요소의 변경은 := 연산자의 이용으로 이루어지는 것이 아니다; 이 연산자는 이름을 객체로 바인딩시키는 데에만 사용된다. 사실 당신이 데이터 구조를 변경하는 일은 절대 없다; 대신 객체로 메시지를 전송하면 객체 스스로 수정해줄 것이다.


예를 들면:

x at: 1


이는 아래를 인쇄한다:

nil


배열의 슬롯은 초기에 'nothing'(스몰토크에선 nil 이라 부른다)으로 설정된다. 첫 번째 슬롯을 숫자 99로 설정해보자:

x at: 1 put: 99


그리고 99가 실제로 그 곳에 존재하도록 확보하자:

x at: 1


그러면 아래가 인쇄된다:

99


이 예제들은 배열을 어떻게 조작하는지 보여준다. 이는 메시지가 전달되는 인자인 일반적인 방식을 보여준다. 대부분의 경우 메시지가 인자를 취하면 그 이름은 ':'으로 끝날 것이다.[5]


x at: 1이라고 말했을 때는 우리가 현재 1의 인자로 x로 바인딩된 객체가 무엇이든 그 곳으로 메시지를 전송하고 있었음을 의미한다. 배열의 경우 배열의 첫 번째 슬롯이 리턴되는 결과를 낳을 것이다.


두 번째 연산, x at: 1 put: 99는 두 개의 인자를 가진 메시지다. 이는 배열에게 두 번째 인자를 첫 번째 인자(1)가 명시한 슬롯에 위치시키라고 말한다. 따라서 첫 번째 슬롯을 재검사했을 때 이는 사실상 99를 포함하고 있는 것이다.


당신이 객체로 전송하는 메시지를 설명하는 약칭이 있다. 그저 메시지 이름을 함께 실행하면 된다. 따라서 우리 배열이 at: 와 at:put: 메시지를 모두 수락한다고 해보자.


배열에는 어느 정도의 정상성 검사(sanity checking)가 빌드되어 있다.

6 at: 1


위의 요청은 오류와 함께 실패한다; 6은 정수이며 색인될 수 없다. 뿐만 아니라,

x at: 21


위의 요청도 실패하는데, 우리가 생성한 배열이 20개 객체를 위한 공간밖에 없기 때문이다.


마지막으로 배열에 보관된 객체는 다른 여느 객체와 같기 때문에 아래와 같은 일을 행할 수 있다:

(x at: 1) + 1


이는 (예제에서 당신이 입력했다고 가정할 때) 100을 인쇄할 것이다.


스몰토크에서 집합(set)

우리가 사용하는 배열은 완료되었으니 새로운 무언가를 우리의 x 변수로 할당할 것이다. 기존 배열에 대해서는 어떤 특별한 일을 할 필요가 없음을 주목하라: 누구도 사용하지 않는다는 사실이 자동으로 감지될 것이며, 메모리는 회수된다. 이는 쓰레기 수집이라고 알려지는데, 주로 메모리가 부족한 상태에서 스몰토크가 실행되고 있음을 발견할 때 실행된다. 따라서 새로운 객체를 얻기 위해서는 아래와 같이 단순하게 실행하면:

x := Set new


빈 집합이 생성된다. 그 내용을 보려면 다음을 이용하면 된다:

x


객체의 유형이 인쇄되고(예: Set), 그 멤버는 괄호 안에 열거된다. 위의 집합은 비어 있기 때문에 아래와 같이 표시될 것이다:

Set ( )


이제 안에 내용물을 채워보자. 숫자 5와 7, 문자열 'foo'를 추가할 것이다. 하나 이상의 문을 사용하는 첫 번째 예제이므로 문 구분자인 마침표 . 를 표시하기 좋은 장소가 되겠다:

x add: 5. x add 7. x add: 'foo'


Pascal과 동일하지만 C와는 달리 문이 종료되는 대신 구분된다. 따라서 하나의 .만을 이용해 하나의 문이 끝나고 다른 문이 시작된다. 따라서 마지막 문인 ^r 다음에는 . 이 없다. 하지만 Pascal과 마찬가지로 스몰토크 역시 마지막 문 다음에 비논리적 문장 구분자를 입력하더라도 스몰토크는 불평하지 않을 것이다.


하지만 스몰토크 축약어를 사용하면 입력하는 수고를 약간은 덜 수 있다:

x add: 5; add: 7; add: 'foo'


이는 앞에서 제시한 행과 정확히 동일한 행동을 취한다. 세미콜론 연산자는 마지막 메시지가 전송된 객체와 동일한 객체로 메시지를 전송하도록 하기 때문이다. 따라서 ; add: 7 은 x add: 7 과 동일한데, x 는 메시지가 마지막으로 전송된 곳이기 때문이다.


그다지 큰 일이 아닌 것처럼 보일지도 모르지만 변수가 x 대신 aVeryLongVariableName 으로 명명되었다면 얼마나 큰일인지 생각해보라! ; 가 수고를 덜어주는 다른 상황은 다시 살펴볼 예정이므로 지금으로선 집합을 계속 살펴보자. 앞에서 든 두 가지 예제 버전 중 하나를 입력하되, 5, 7, 'foo'가 입력되었는지 확실히 하라:

x


이제 우리의 데이터를 포함하고 있음을 볼 수 있다:

Set ('foo' 5 7)


여기서 어떤 대상을 두 번 추가하면 어떨까? 문제없다-집합 내에 그대로 유지된다. 그러면 집합은 커다란 체크리스트가 된다-그것이 있거나 없거나, 둘 중 하나다. 정확해 말해:

x yourself


add: 를 이용해 집합으로 추가한다고 하면 remove: 를 이용해 꺼낼 수 있다.

x remove: 5
x printNl


위를 시도해보면 이제 집합은 아래를 인쇄한다:

Set ('foo' 7)


'5'는 사실상 집합에서 사라졌다.


집합으로 할 수 있는 많은 일들 중 하나-멤버십 확인-를 이제 마무리하겠다. 아래를 시도해보라:

x includes: 7
x includes: 5


x가 7은 포함하지만 5는 포함하지 않음을 볼 수 있다. 답은 true 또는 false로 인쇄됨을 주목하라. 다시 말하지만, 리턴되는 것은 객체이다-이번 경우 부울(Boolean)로 알려진 객체이다. 부울의 사용은 후에 살펴볼 것이며, 현재로선 부울은 true 또는 false의 값만 가질 수 있는 객체에 지나지 않는다고만 말할 것이다. 따라서 방금 제시한 질문처럼 예/아니오 질문에 대한 답으로는 매우 유용하다. 또 다른 유형의 데이터 구조를 살펴보자.


Dictionaries

Dictionaries는 매우 유용한 유형의 컬렉션이다. 정규 배열을 이용 시 정수로 색인해야 한다. dictionary을 이용 시에는 어떤 객체와도 색인이 가능하다. 따라서 dictionary은 어떤 정보 조각을 다른 조각과 상관시키는 데 있어 매우 강력한 방법을 제공한다. 유일한 단점은 단순한 배열에 비해 좀 덜 효율적이라는 점이다. 아래를 실행해보라:

y := Dictionary new
y at: 'One' put: 1
y at: 'Two' put: 2
y at: 1 put: 'One'
y at: 2 put: 'Two'


이는 dictionary를 일부 데이터로 채운다. 데이터는 사실 키(key)와 값의 쌍으로 보관된다 (키는 당신이 at: 에 제공하는 것으로, 슬롯을 명시한다; 값은 슬롯에 실제로 보관되는 것이다). 정수뿐만 아니라 문자열까지 키와 값으로 명시할 수 있음을 주목하라. 사실 우리가 원하는 어떤 유형의 객체든 둘 중 하나로 사용할 수 있다-dictionary는 신경 쓰지 않는다.


이제 각 키를 값으로 매핑할 수 있다:

y at: 1
y at: 'Two'


이는 각각 아래를 인쇄한다:

'One'
2


dictionary에게 스스로를 인쇄하도록 요청할 수도 있다:

y


그러면 아래가 인쇄된다:

Dictionary (1->'One' 2->'Two' 'One'->1 'Two'->2)


각 쌍에서 첫 번째 멤버는 키, 두 번째 멤버는 값이다. 이제 우리가 생성한 객체를 마지막으로 살펴보고 잊어버리도록 하자.

y
x!


느낌표는 GNU Smalltalk로부터 두 변수에 대한 지식을 삭제시켰다. 이를 다시 요청하면 nil 만 리턴할 것이다.


생각 마무리하기

스몰토크에서 어떻게 몇몇 강력한 데이터 구조를 제공하는지를 살펴보았을 것이다. 또한 스몰토크 자체가 어떻게 이러한 기능들을 이용해 언어를 구현하는지도 확인했다. 그러나 이는 빙산의 일각에 불과하다-스몰토크는 사용하기에 '깔끔한' 기능의 집합체 그 이상이기 때문이다. 자동으로 이용할 수 있는 객체와 메서드는 당신이 프로그램을 빌드하는 기반의 시작일 뿐이다-스몰토크는 객체와 메서드를 시스템으로 추가하여 다른 모든 것과 함께 사용하도록 해준다. 스몰토크에서 프로그래밍의 미(art)는 객체와 관련된 문제를 바라보고, 좋은 결과를 낳는 기존 객체 타입을 이용하며, 스몰토크를 새로운 타입의 객체로 개선하는 데에서 발생하는 미(art)다. 이제 스몰토크 조작의 기본 내용을 학습했으니 프로그래밍의 객체 지향 기법을 살펴볼 때가 되었다.


스몰토크 클래스 계층구조

스몰토크에서 프로그래밍을 하면서 새로운 유형의 객체를 생성하고 다양한 메시지들이 이러한 객체들에게 할 일을 정의해야 하는 경우가 종종 있다. 다음 장에서는 새 클래스를 몇 가지 생성할 것인데, 우선 스몰토크가 포함하는 타입이나 객체를 어떻게 구성하는지 이해할 필요가 있다. 실행시킬 실제 스몰토크 코드가 없이 순전히 '개념'에 관한 장이므로 짧게 핵심만 소개하겠다.

Object 클래스

스몰토크는 그 모든 클래스를 트리 계층구조로서 구성한다. 이 계층구조의 최상단에는 Object 클래스가 존재한다. 그 바로 아래에는 좀 더 구체적 클래스로, 우리가 작업한 클래스-문자열, 정수, 배열 등-들이 여기에 속한다. 이러한 클래스들은 유사성을 기반으로 그룹화된다; 가령 객체의 타입이 다소 비슷한 객체들은 Magnitude 로 알려진 클래스에 속할 수 있겠다.


새 객체를 생성 시 처음으로 해야 할 작업 중 하나는 바로 자신의 객체가 계층구조에서 어디에 해당하는지 확인하는 일이다. 이 문제에 대한 답을 알아내는 것은 과학만큼이나 예술에 속하는데, 이를 성취할 수 있는 엄격한 규칙은 존재하지 않는다. 이러한 구성이 어떻게 중요한지 느낌을 알 수 있도록 세 가지 유형의 객체를 살펴볼 것이다.


Animals

세 가지 유형의 객체가 있는데 각각 Animals(동물), Parrot(앵무새), Pigs(돼지) 를 나타낸다고 상상해보자. 우리 메시지는 eat(먹다), sing(노래하다), snort(킁킁거리다)가 될 것이다. 이러한 객체를 스몰토크 계층구조로 삽입하는 1차 패스는 아래와 같은 조직을 생성할 것이다:

Object
	Animals
	Parrots
	Pigs


이는 Animals, Parrots, Pigs 모두 Object 의 직계 자식(descendant)이며, 서로의 자식은 아니다.


이제 각 동물이 각 메시지 유형에 어떻게 응답하는지를 정의해야 한다.

Animals
eat –> Say ''I have now eaten'
sing –> Error
snort –> Error
Parrots
eat –> Say ''I have now eaten'
sing –> Say ''Tweet'
snort –> Error
Pigs
eat –> Say ''I have now eaten"'
sing –> Error
snort –> Say ''Oink'


어떻게 eat 의 액션을 표시해야 하도록 유지했는지 주목하라. 숙련된 객체 디자이너라면 지금쯤 우리가 계층구조를 올바르게 준비시키지 않았다는 증거를 즉시 인지할 것이다.


다른 구성을 시도해보자:

Object
	Animals
		Parrots
			Pigs


즉, Parrots는 Animals에서 상속되고, Pigs는 Parrots에서 상속된다. 이제 Parrots는 Animals로부터 모든 액션을 상속하고, Pigs는 Parrots와 Animals에서 모두 상속한다. 이러한 상속 때문에 이제 이전 집합과의 중복을 막아주는 새로운 액션 집합을 정의해도 좋다:

Animals
eat –> Say ''I have now eaten'
sing –> Error
snort –> Error
Parrots
sing –> Say ''Tweet'
Pigs
snort –> Say ''Oink'


Parrots와 Pigs 모두 Animals에서 상속되기 때문에 eat 액션을 한 번만 정의하면 된다. 하지만 클래스 구성에서 한 가지 실수를 저질렀다-Pig(돼지)에게 sing(노래하라)고 말한다면 어떤 일이 발생할까? Pigs를 Parrots의 상속자로 넣었기 때문에 'Tweet(짹짹)'이라고 답할 것이다.


마지막으로 한 가지 구성을 더 살펴보자:

Object
	Animals
		Parrots
		Pigs


이제 Parrots와 Pigs는 Animals에서 상속되지만 서로 상속되지는 않는다. 마지막으로 간략한 액션 집합도 하나 정의해보자:

Animals
eat –> Say ''I have eaten'
Parrots
sing –> Say ''Tweet'
Pigs
snort –> Say ''Oink'


부적절한 메시지가 생략되었다는 점이 변경되었다. 스몰토크가 만일 객체 또는 그 조상들 중 하나가 메시지를 알지 못한다는 사실을 인식하면 자동으로 오류를 제공할 것이다-따라서 이러한 일을 당신이 행할 필요가 없다. 이제 Pig로 sing 을 전송하면 'Tweet'이라고 말하지 않는다-대신 스몰토크 오류를 발생시킬 것이다.


클래스 계층구조의 마지막 행

클래스 계층구조는 특정 객체가 그 조상들의 코드를 상속할 수 있도록 해주는 관계로 객체들을 조직하도록 해주는 데에 목적이 있다. 효과적인 타입의 조직을 확인했다면 특정 기법은 한 번만 실행된 후 그 아래 자식에게 상속되어야 한다는 사실을 눈치 챌 것이다. 이 덕분에 코드를 작게 유지할 수 있고, 특정 알고리즘에 발생한 버그를 한 장소에서만 수정해도 괜찮은 것이다-그러면 그것을 사용하는 모든 사람은 fix(수정내용)를 상속하기만 하면 된다.


경험이 늘어나면서 객체의 추가에 대한 자신의 결정도 변한다는 사실을 발견할 것이다. 기존 객체와 메시지 집합에 익숙해질수록 자신의 선택은 점점 더 기존 객체와 메시지에 '들어맞을' 것이다. 하지만 스몰토크 전문가들조차 이쯤 되면 잠시 멈추고 주의 깊게 생각하므로, 첫 번째 선택이 까다롭고 오류 발생이 쉽다 하더라도 주눅 들지 말길 바란다.


객체의 새 클래스 생성하기

앞의 여러 장에 걸쳐 제시한 기본 기법들을 이용해 첫 스몰토크 프로그램을 시작할 준비가 되었다. 본장에서는 세 가지 새 객체 타입(클래스로 알려짐)을 서로 결합하기 위해 상속이라는 스몰토크 기법을 이용해 객체 타입을 구성하고, 이러한 클래스에 속하는 새 객체들을 생성하여 (클래스의 인스턴스 생성이라 알려짐) 객체들에게 메시지를 전송할 것이다.


이 모든 것은 가상의 가정 재무 시스템(toy home-finance accounting system)을 구현하여 연습할 것이다. 전반적인 현금을 추적하고, 수표 계좌(checking account)와 저축 계좌(saving account)의 특별 처리를 가질 것이다. 이 시점부터 우리는 다음 여러 절에서 사용될 클래스들을 정의할 것이다. 전체 지침을 한 번의 스몰토크 세션에서 실행할 가능성은 거의 없을 것으로 추측되니 앞의 예제들을 모두 재입력할 필요가 없도록 스몰토크의 상태를 잠시 저장하고 다시 시작하길 권한다. GNU Smalltalk의 현재 상태를 저장하기 위해서는 아래를 입력하라:

ObjectMemory snapshot: 'myimage.im'


그리고 추후 셸(shell)에서 이 '스냅샷'으로부터 스몰토크를 재시작하려면 아래처럼 진행한다:

localhost$ gst –I myimage.im


이러한 스냅샷은 최근에 메가바이트보다 조금 더 많은 시간이 소요되고, 당신이 추가한 모든 변수, 클래스, 정의를 포함한다.


새 클래스 생성하기

새 클래스를 어떻게 추가할까? 지금쯤이면 지겨울 정도로 쉬운 문제일 것이다-객체로 메시지를 전송한다. 첫 번째 '커스텀' 클래스를 생성하는 방법으로 우리는 아래 메시지를 전송할 것이다:

Object subclass: #Account.
Account instanceVariableNames: 'balance'.


조금 길다, 그렇지 않은가? GNU Smalltalk는 이를 더 간단하게 작성하는 방식을 제공하지만 우선은 이걸로 만족하자. 개념적으로 보더라도 그리 나쁘진 않다. 스몰토크 변수 Object 는 시스템 상의 모든 클래스의 할아버지에 결합되어 있다. 우리가 여기서 할 일은 Object 클래스에게 Account 라고 알려진 서브클래스를 추가하고 싶다는 뜻을 알리는 일이다. 그렇다면 instanceVariableNames: 'balance'는 새 클래스에게 그것의 객체(인스턴스)마다 balance라 명명된 숨은 변수를 갖게 될 것이라고 말한다.


클래스 문서화하기

다음 단계는 설명을 클래스와 관계시키는 일이다. 이는 새 클래스로 메시지를 전송하여 실행한다:

Account comment:
'I represent a place to deposit and withdraw money'


설명은 모든 스몰토크 클래스와 연관되어 있고, 본인이 정의하는 새 클래스마다 설명을 추가하기에 양호한 형태로 간주된다. 주어진 클래스에 대한 설명을 얻기 위해서는 다음을 이용한다:

Account comment


자신의 문자열이 다시 인쇄된다. Integer 클래스로도 시도해보라:

Integer comment


그러나 클래스를 정의하는 또 다른 방법이 있다. 이 방법 역시 전송하는 객체로 해석하지만 전형적인 프로그래밍 언어 또는 스크립팅 언어와 더 닮았다:

Object subclass: Account [
    | balance |
        <comment:
        'I represent a place to deposit and withdraw money'>
]


이제 클래스가 생성되었다. 주석의 수정 등의 이유로 다시 접근하길 원한다면 아래와 같이 실행할 것이다:

Account extend [
    <comment:
    'I represent a place to withdraw money that has been deposited'>
]


이는 스몰토크에게 서브클래스를 생성하는 대신 기존 클래스를 고르도록 지시한다.


클래스에 대한 메서드 정의하기

클래스를 생성했지만 우리를 위해 작업을 대신하기엔 아직 무리다-클래스가 처리할 수 있는 메시지를 몇 가지 먼저 정의해야 한다. 인스턴스 생성을 위한 메서드의 정의부터 시작해보자:

Account class extend [
    new [
        | r |
        <category: 'instance creation'>
        r := super new.
        r init.
        ^r
    ]
]


여기서 중요한 사항은 다음과 같다:

  • Account class 란 Account 클래스 자체로 전송되어야 할 메시지를 정의하고 있음을 의미한다.
  • <category: 'instance creation'>은 더 많은 문서 지원이다; 우리가 정의하고 있는 메서드가 Account 타입의 객체 생성을 지원함을 나타낸다.
  • new로 시작하고 [끝이 나는] 텍스트는 new 메시지에 어떤 액션을 취한 것인지 정의하였다. 해당 정의부를 입력하면 GNU Smalltalk는 단순히 프롬프트를 하나 더 제공하지만 당신의 메서드는 컴파일되었고 사용될 준비가 되어 있다. GNU Smalltalk는 성공적인 메서드 정의부에 대해서는 꽤 조용한 편이다-하지만 문제가 발생하면 엄청난 오류 메시지를 받게 될 것이다!


다른 스몰토크에 익숙하다면 메서드의 본체는 항상 괄호 안에 표기됨을 주목하라.


이러한 메서드가 어떻게 작용하는지 설명하는 최선의 방법은 하나씩 해보는 방법뿐이다. 아래와 같은 명령 행으로 Account 라는 새 클래스로 메시지를 전송했다고 가정해보자:

Account new


Account 는 new 라는 메시지를 수신하고, 이 메시지를 어떻게 처리하는지 검색한다. 우리의 새 정의를 찾는다면 실행을 시작한다. 첫 행인 l r l 은 우리가 생성한 객체에 대한 플레이스 홀더로서 사용할 수 있는 r 이라는 지역 변수를 생성한다. r은 메시지의 처리가 끝나자마자 없어질 것이다; 객체의 사용이 중지되자마자 사라지는 balance와 차이를 주목하라. 그리고 앞의 예제들과 달리 여기서는 당신이 지역 변수를 명시적으로 선언한다는 사실도 주목하라.


사실상 첫 번째 단계는 객체를 실제로 생성하는 단계다. r := super new 행은 놀라운 기술을 이용해 이를 실행한다. super 라는 단어는 new 메시지가 본래부터 전송되는 것과 동일한 객체를 (Account 기억하는가?) 나타내지만, 스몰토크가 메서드를 검색할 때는 계층구조에서 현재 수준보다 한 수준 높은 곳부터 시작한다. 따라서 Account 클래스 내 메서드의 경우 이것은 Object 클래스가 되며 (Account 클래스가 상속되는 클래스는 Object이므로-Account 클래스를 어떻게 생성하는지 살펴보았던 때로 돌아가서 참고하라), Object 클래스의 메서드는 #new 메시지에 대한 응답으로 일부 코드를 실행한다. 마침내 Object는 #new 메시지가 전송되면 객체를 실제로 생성할 것이다.


천천히 다시 살펴보자: Account 메서드 #new 는 새 객체가 생성될 때 뭔가를 하고 싶어하는 동시에 그의 부모 클래스가 같은 이름으로 된 메서드로 작업을 하도록 두고 싶어한다. r := super new라고 말함으로써 그(#new)는 부모가 객체를 생성하도록 두고, 그것을 스스로 변수 r에 부착한다. 따라서 해당 코드 행이 실행되고 나면 우리는 완전히 새로운 타입의 Account 객체를 갖게 되고, r이 바인딩되어 있다. 시간이 지나면 더 이해가 쉬워질테니 지금으로선 의아하더라도 그러려니 하고 넘어가길 바란다.


이제 새 객체를 가졌지만 올바로 설정하지 않았다. 본장이 시작할 때 살펴본 숨은 변수 balance 를 기억하는가? super new 는 아무 것도 포함하지 않은 balance 필드의 객체를 제공하지만 우리는 balance(계좌 잔액) 필드가 0부터 시작하길 바란다.[6]


그렇다면 우리가 해야 할 일은 객체에게 준비하도록 요청하는 일이다. r init 라고 말함으로써 우리는 새 Account 로 init 메시지를 전송한다. 해당 메서드는 다음 절에서 정의할 것이며, 지금은 init 메시지를 전송하면 Account 가 준비될 것이라고 가정하기만 하자.


마지막으로 ^r 을 덧붙였다. 영어로 이것은 r이 달라 붙어 있는 리턴을 의미한다. 즉, Account로 new 메시지를 보낸 이가 누구든 완전히 새로운 account를 얻게 될 것이란 뜻이다. 그와 동시에 우리의 임시 변수 r의 존재는 사라진다.


인스턴스 메서드 정의하기

Account 객체에 init 메서드를 정의하여 위에서 정의한 new 메서드가 작동하도록 할 필요가 있다. 스몰토크 코드는 다음과 같다:

Account extend [
    init [
        <category: 'initialization'>
        balance := 0
    ]
]


이전 메서드 정의와 약간 닮았지만 앞서 소개한 첫 행에서는 Account class extend라고 말하는데 방금 제시한 코드는 Account extend라고만 되어 있다.


그 차이는 전자의 경우 Account로 직접 전송된 메시지에 대한 메서드를 정의한 것이고, 후자는 Account 객체가 생성되고 나서 객체로 전송되는 메시지에 대한 메서드의 정의다.


init 라 명명된 메서드는 하나의 행, balance :=0 만 포함한다. 이는 숨은 변수 balance 를 (사실상 인스턴스 변수라 불림) 0으로 초기화하는데, 계좌 잔액에 알맞은 행위다. 메서드명이 ^r 또는 그와 비슷한 것으로 끝나지 않음을 주목하라: 이 메시지는 메서드 전송자에게 값을 리턴하지 않는다. 프로그래머가 리턴 값을 명시하지 않으면 스몰토크는 현재 실행 중인 객체로 리턴 값을 디폴트(default)한다. 프로그래밍의 명확성을 위해, 리턴 값이 후에 사용되도록 의도하는 경우 self를 명시적으로 리턴하는 것을 고려할 수도 있다.[7]


Account 살펴보기

Account 클래스의 인스턴스를 살펴보자:

a := Account new


이것이 무엇인지 짐작이 가는가? Smalltalk at: #a put: <something> 은 스몰토크 변수를 생성한다. 그리고 Account new는 새 Account를 생성하여 그것을 리턴한다. 따라서 위의 행은 a 로 명명된 스몰토크 변수를 생성하고, 이를 새 Account로 덧붙인다-하나의 행만으로. 뿐만 아니라 방금 생성한 Account 객체를 인쇄하기도 한다:

an Account


음...그다지 많은 정보를 제공하지 않는다. 문제는 Account에게 어떻게 인쇄해야 할 것인지를 알려주지 않았기 때문에 기본 시스템 printNl 메서드를 얻게 될 것이란 점이다-이 메서드는 객체가 무엇을 포함하는지가 아니라 객체가 무엇인지를 알려준다. 따라서 객체가 무엇을 포함하는지를 알려줄 메서드를 추가해야겠다:

Account extend [
    printOn: stream [
        <category: 'printing'>
        super printOn: stream.
        stream nextPutAll: ' with balance: '.
        balance printOn: stream
    ]
]


이제 다시 시도해보자:

a


그러면 아래가 인쇄된다:

an Account with balance: 0


조금 이상해 보일지도 모르겠다. printOn: 이라는 새 메서드를 추가했고, printNl 메시지가 다르게 행동하기 시작한다. printOn: 메시지는 주요 인쇄 함수(central printing function)인 것으로 드러난다-우선 정의하고 나면 다른 인쇄 메서드는 모두 이 함수를 호출하게 되어 있다. 그 인자는 인쇄를 해야 할 장소이다-주로 Transcript 변수가 해당한다. 해당 변수는 보통 자신의 단말기에 엮여(hooked) 있으므로 화면에 인쇄할 수도 있다.


super printOn: stream 는 이전에 부모 클래스가 했던 일을 이번에도 알아서 하도록 둔다-즉, 우리의 타입이 무엇인지 인쇄하는 일 말이다. 인쇄내용에서 an Account 부분은 여기서 비롯된 것이다. stream nextPutAll: 'with balance: '는 with balance: 문자열을 생성하고, 이를 스트림으로 인쇄하기도 한다; 여기서는 printOn: 을 사용하지 않는데, 문자열이 따옴표 안으로 들어가기 때문이다. 마지막으로 balance printOn: stream은 balance 변수로 연결된 객체가 무엇이든 자신을 스트림으로 인쇄하도록 요청한다. balance를 0으로 설정하므로 0이 인쇄된다.


자금 이동하기

이제 account를 생성하여 살펴볼 수 있다. 하지만 현재 우리 계좌 잔액은 항상 0이 될 것이다-이 무슨 비극인가! 우리가 생성할 마지막 메서드들이 돈을 저금하고 소비하도록 해줄 것인데, 그 방법은 엄청 간단하다:

Account extend [
    spend: amount [
        <category: 'moving money'>
        balance := balance - amount
    ]
    deposit: amount [
        <category: 'moving money'>
        balance := balance + amount
    ]
]


이러한 메서드들을 이용해 이제 예금을 하고 소비를 할 수 있다. 아래 연산을 시도해보라:

a deposit: 125
a deposit: 20
a spend: 10


다음은 무엇인가?

이제 우리에겐 'Account' 라는 유개념이 있다. 계좌를 생성하고, 계좌 잔액을 확인하고, 돈을 예금 또는 인출할 수도 있다. 이는 훌륭한 기반을 제공하지만 특별한 account 타입이 원할 법한 중요한 정보는 빠져 있다. 서브클래스를 이용해 이 문제를 해결하는 방법을 다음 절에서 살펴볼 것이다.


Account 클래스의 두 서브클래스

이번 절에서는 앞 절의 내용을 이어 스몰토크에서 클래스와 서브클래스를 생성하는 방법을 보이고자 한다. 여기서는 Account의 특별한 서브클래스로 Checking과 Savings를 생성할 것이다. Account의 기능을 계속해서 상속할 것이지만 특별한 유형의 account를 더 잘 관리하도록 두 가지 유형의 객체를 맞출 것이다.

Savings 클래스

Savings 클래스를 Account의 서브클래스로서 생성한다. 이는 Account와 마찬가지로 현금을 보유하지만 우리가 모델링할 추가 프로퍼티를 갖고 있다: 계좌 잔액을 기반으로 이자가 지급된다. 우선 Account의 서브클래스로서 Savings 클래스를 생성한다.

Account subclass: Savings [
    | interest |


위의 행만으로도 정보를 알려준다: 인스턴스 변수 interest는 지급된 이자를 축적할 것이란 점 말이다. 따라서 부모 클래스 Account 로부터 상속한 메시지 spend: 와 deposit: 외에 예금 이자를 포함시키기 위한 메서드 하나와 interest 변수를 클리어(clear)하는(세금을 지급한 후 매년 실행하는) 방법을 정의할 필요가 있다. 먼저 새 계좌를 할당하기 위한 메서드를 정의한다-이자 필드가 0부터 시작하도록 확실히 해야 한다.


이는 위에서 아직 닫지 않은 Account subclass: Savings 범위 내에서 이행할 수 있겠다.

init [
    <category: 'initialization'>
    interest := 0.
    ^super init
]


부모 클래스가 new 메시지를 처리하고 적절한 크기로 된 새 객체를 생성했다는 사실을 기억해보자. 생성 후 부모 클래스는 새 객체로 init 메시지까지 전송했다. 새 객체는 Account의 서브클래스로서 init 메시지를 먼저 수신할 것이다; 이는 자신의 인스턴스 변수를 준비시킨 후 init 메시지를 상향으로 전달하여 그의 부모 클래스에게 초기화 일부를 처리하도록 한다.


새 Savings 계좌가 생성되었으니 해당 계좌를 특별히 처리하는 메서드를 두 가지 정의할 수 있다:

interest: amount [
    interest := interest + amount.
    self deposit: amount
]

clearInterest [
    | oldinterest |
    oldinterest := interest.
    interest := 0.
    ^oldinterest
]


이제 완료되었으니 클래스 범위(class scope)를 닫자:

]


첫 번째 메서드는 실행 중인 이자 총액에 amount를 추가함을 의미한다. self deposit: amount 행은 우리에게 메시지를 보내줄 것을 스몰토크에게 알리는데, 이번 경우 deposit: amount 메시지가 되겠다. 그러면 스몰토크는 deposit: 에 대한 메서드를 검색하고, 부모 클래스인 Account를 찾는다. 해당 메서드를 실행하면 전체 잔액을 업데이트한다.[8]


여러분 중에는 이 메서드를 balance := balance + amount 와 같이 단순한 것으로 대체하지 않는 이유가 궁금한 사람도 있을 것이다. 이에 대한 답은 일반적으로는 객체 지향 언어, 그 중에서도 스몰토크의 철학 중 하나 때문이다. 우리의 목표는 무언가를 한 번만 실행하도록 기법을 인코딩한 후 필요할 때마다 기법을 재사용하는 데에 있다. 직접 인코딩된 balance := balance + amount 가 있었다면 예금으로부터 잔액을 업데이트하는 방법을 아는 장소가 두 군데가 존재할 것이다. 별로 크게 차이나지 않는 것처럼 보일지도 모른다. 하지만 후에 예금 횟수를 세기로 결정한다면 어떨까? 계좌 잔액을 업데이트해야 하는 곳마다 balance := balance + amount 를 인코딩해야 한다면 예금 횟수를 업데이트하기 위해 모두 추적을 해야 할 것이다. self에 deposit: 메시지를 전송함으로써 이 메서드를 한 번만 업데이트하면 된다; 그러면 이 메시지의 각 전송자가 자동으로 계좌 잔액을 업데이트하기 위해 올바른 최신 기법을 얻는다.


두 번째 메서드인 clearInterest는 좀 더 간단하다. 먼저 우리는 현재 이자 금액을 보유하기 위해 임시 변수 oldinterest를 생성한다. 그리고 매년 새롭게 시작하기 위해 이자를 0으로 조정한다(zero out). 마지막으로 우리는 기존 이자를 결과로 리턴하여 연말 회계원이 우리가 얼마를 모았는지 볼 수 있도록 한다.[9]


Checking 클래스

Account의 두 번째 서브클래스는 수표발행(checking) 계좌다. 여기서 두 가지 면을 계속 파악할 것이다:

  • 수표 발행 번호
  • 수표장(checkbook)에 남은 수표 개수


이를 Account의 또 다른 서브클래스로 정의할 것이다.

Account subclass: Checking [
    | checknum checksleft |


두 개의 인스턴스 변수가 있지만 둘 중 하나만 초기화시키면 된다-남은 수표가 없다면 현재 수표 발행 번호는 중요하지 않다. 부모 클래스인 Account가 우리에게 init 메시지를 전송할 것임을 명심하라. 필요한 것은 모두 부모 클래스가 제공할 것이므로 클래스 특정적인 new 함수가 필요 없다.

init [
    <category: 'initialization'>
    checksleft := 0.
    ^super init
]


Savings에서와 마찬가지로 슈퍼클래스인 Account 로부터 대부분의 기능을 상속받는다. 초기화를 위해 checknum 만 남겨두되 수표장 내 수표의 수는 0으로 설정한다. 종료는 부모 클래스에게 고유의 초기화를 시키면 된다.


수표 사용하기

이번 절은 수표장을 통해 돈을 사용하기 위한 메서드를 추가하면서 마무리 지을 것이다. 메시지를 취하고 변수를 업데이트하는 기법은 익숙할 것이다:

    newChecks: number count: checkcount [
        <category: 'spending'>
        checknum := number.
        checksleft := checkcount
    ]

    writeCheck: amount [
        <category: 'spending'>
        | num |
        num := checknum.
        checknum := checknum + 1.
        checksleft := checksleft - 1.
        self spend: amount.
        ^ num
        ]
]


newChecks: 는 수표장을 수표로 채운다. 우리는 어떤 수표 발행 번호로 시작하는지를 기록하고, 수표장에 수표 장수(number of checks)를 업데이트한다.


writeCheck: 는 다음 수표 번호를 기록한 후 수표 발행 번호를 올리고 수표 개수는 줄인다. self spend: amount 메시지는 우리 객체에 spend: 메시지로 응답한다. 이에 따라 스몰토크는 그 메서드를 검색한다. 이후 메서드는 부모 클래스인 Account에서 발견되고, 계좌 잔액은 소비를 반영하여 업데이트된다.


아래의 예제를 시도해볼 수 있겠다:

c := Checking new
c deposit: 250
c newChecks: 100 count: 50
c writeCheck: 32
c


한 가지 재밌는 점은, checking에 특정적인 정보를 볼 수 있도록 checking 클래스에 printOn: 메시지를 추가하고자 할 수도 있다는 사실이다.


이번 절에서는 자신만의 클래스의 서브클래스를 생성하는 방법을 살펴보았다. 새 메서드를 추가하고, 부모 클래스로부터 메서드를 상속하였다. 이러한 기법들은 문제에 대한 해결책을 구축하기 위한 구조 대부분을 제공한다. 다음에 소개될 여러 절에 걸쳐서는 언어 메커니즘과 타입을 상세히 알아보고, 스몰토크에서 작성된 소프트웨어를 디버깅하는 방법을 상세히 제공하겠다.


코드 블록

앞 절의 Account/Saving/Checking 예제는 몇 가지가 부족하다. 수표와 그 가치의 기록이 없다. 뿐만 아니라 수표가 없을 때에도 수표를 사용할 수 있다는 문제가 더 심각하다-수표 장수에 대한 Integer 값이 조용히 음수가 될 수도 있기 때문이다! 이러한 문제를 해결하기 위해서는 좀 더 정교한 제어 구조가 필요하겠다.

조건과 의사결정

먼저 너무 많은 수표를 쓰지 않도록 코드를 조금 추가해보자. Checking 클래스에 대한 혀재 메서드를 단순히 업데이트하는 것부터 시작할 것이다; 앞 절의 메서드를 입력했다면 새로운 정의가 기존 정의를 덮어쓸 것이다.

Checking extend [
    writeCheck: amount

        | num |

        (checksleft < 1)
            ifTrue: [ ^self error: 'Out of checks' ].
        num := checknum.
        checknum := checknum + 1.
        checksleft := checksleft - 1.
        self spend: amount
        ^ num
    ]
]


다음은 새로운 두 행이다:

(checksleft < 1)
    ifTrue: [ ^self error: 'Out of checks' ].


언뜻 보기엔 완전히 새로운 구조처럼 보인다. 하지만 다시 살펴보라! 유일한 새 구성체는 사각 괄호밖에 없으며, 메서드를 감쌀뿐만 아니라 메서드 내부에 나타나고 있다.


첫 행은 단순한 부울 표현식이다. checksleft는 Checking 클래스에 의해 초기화되는 정수다. 여기에 < 메시지와 인자 1이 전송된다. 현재 checksleft에 바인딩된 숫자는 자신을 1에 비하며, 1보다 큰지 적은지를 알려주는 부울 객체를 리턴한다.


true 또는 false 값을 가질 수 있는 이 부울(Boolean)에는 코드 블록이라 불리는 인자를 가진 ifTrue: 메시지가 전송된다. 코드 블록은 다른 여느 것과 마찬가지로 객체이다. 하지만 숫자 또는 집합을 보유하는 대신 실행 가능한 문을 보유한다. 그렇다면 ifTrue: 메시지에 인자인 코드 블록과 이 부울은 무슨 관계가 있을까? 이는 대상이 어떤 부울이냐에 따라 달려 있다! 객체가 true 객체라면 자신에게 전달된 코드 블록을 실행한다. false 객체로 밝혀지면 실행 가능한 블록 없이 리턴한다. 따라서 전형적인 조건부 구성체는 스몰토크에서 true 값에 따라 표시된 코드 블록을 실행 또는 실행하지 않는 부울 객체로 대체되었다.[10]


소개한 예제의 경우 블록 내 실제 코드는 현재 객체에게 오류 메시지를 전송한다. error: 는 부모 클래스인 Object에 의해 처리되어 사용자가 수표를 너무 많이 사용하려 하면 적절한 경고(complaint)를 팝업시킬 것이다. 일반적으로 스몰토크에서 치명적인 오류를 처리할 때는 스스로에게 오류 메시지를 전송하는 방법을 사용하여 (self 의사 변수를 통해) Object로부터 상속된 오류 처리 메커니즘이 해결하도록 한다.


짐작하겠지만 부울 객체가 수락하는 메시지로 ifFalse: 라는 메시지가 있다. 논리(logic)가 반전(reversed)되었다는 점만 제외하면 ifTrue: 와 정확히 동일하게 작용한다; 부울값 false 는 코드 블록을 실행하고, 부울값 true는 실행하지 않을 것이다.


조건부를 표현하는 이러한 메서드의 이용에 시간을 투자하길 바란다. 자신의 수표장을 실행하면서 조건부 함수를 직접 호출할 수도 있다:

true ifTrue: [ 'Hello, world!' printNl ]
false ifTrue: [ 'Hello, world!' printNl ]
true ifFalse: [ 'Hello, world!' printNl ]
false ifFalse: [ 'Hello, world!' printNl ]


반복과 컬렉션

이제 어느 정도 정상성 검사가 준비되었으니 우리가 사용하는 수표의 로그를 보관하는 일만 남았다. 이는 Checking 클래스로 Dictionary 객체를 추가하고, 수표를 그 안에 로깅하여, 수표 사용 내역(checking history)을 문의하기 위한 메시지를 몇 가지 제공함으로써 완료하고자 한다. 하지만 이러한 기능의 개선은 매우 흥미로운 질문으로 이어진다-객체의 '모양'은 언제 변경하고 (이번 경우 Checking 클래스로 우리 dictionary를 새 인스턴스 변수로서 추가함으로써), 기존 클래스나 그 객체에는 어떤 일이 발생할까? 그에 대한 답으로는 '오래된 객체들은 새로운 모양을 유지하도록 변형되고, 모든 메서드는 새로운 모양과 작동하도록 재컴파일된다'가 되겠다. 새 객체는 기존 객체와 정확히 같은 모양을 갖겠지만, 기존 객체들이 올바르지 않게 초기화되는 일이 발생할지도 모른다 (새로 추가된 변수가 단순히 nil로 놓았다는 이유만으로). 이는 매우 곤혹스러운 행위로 이어지기도 하므로 기존 객체를 모두 싹 없애버린 후 변경하는 편이 보통은 최선이다.


이것이 가상의 객체 회계 시스템 이상의 것이라면 아마 객체의 저장, 새 클래스로의 변환, 객체를 새 포맷으로 읽어오기 등의 작업도 수반될 것이다. 지금은 이러한 마음을 잠시 접어두고 최신 Checking 클래스를 정의하도록 해보자.

Checking extend [
    | history |


앞서 수표 계좌를 정의했을 때와 동일한 구문인데, 유일한 차이는 이번에 extend로 시작한다는 점이다 (클래스가 이미 존재하므로). 따라서 우리가 정의한 두 개의 인스턴스 변수도 남아 있으며, 새로운 history 변수를 추가한다; 오래된 메서드는 오류 없이 재컴파일될 것이다. 이제 사실상 새 클래스를 오래된 이름으로 정의하고 있으므로 객체가 처리할 수 있는 메시지마다 정의부를 채워야한다.


새로운 Checking 인스턴스 변수를 이용해 수표 사용 내역을 기록할 준비를 마쳤다. 첫 번째 변경내용은 init 메시지의 처리에 있을 것이다:

init [
    <category: 'initialization'>
    checksleft := 0.
    history := Dictionary new.
    ^ super init
]


위는 Dictionary 를 제공하고, 그것을 새 history 변수로 엮는다(hook).


그 다음 메서드는 각 수표를 사용할 때마다 수표를 기록한다. 수표 사용에 좀 더 많은 정상성 검사를 추가하면서 메서드는 좀 더 복잡해진다.

writeCheck: amount [
    <category: 'spending'>
    | num |

    "Sanity check that we have checks left in our checkbook"
    (checksleft < 1)
        ifTrue: [ ^self error: 'Out of checks' ].

    "Make sure we've never used this check number before"
    num := checknum.
    (history includesKey: num)
        ifTrue: [ ^self error: 'Duplicate check number' ].

    "Record the check number and amount"
    history at: num put: amount.

    "Update our next checknumber, checks left, and balance"
    checknum := checknum + 1.
    checksleft := checksleft - 1.
    self spend: amount.
    ^ num
]


마지막 writeCheck: 버전에 세 가지를 추가하였다. 첫째, 루틴이 약간은 복잡해보여 주석을 추가하였다. 스몰토크에서 작은따옴표는 문자열에 사용되고 큰따옴표는 주석을 닫는 데 사용된다. 코드의 각 세션 앞에 주석을 추가하였다.


둘째, 사용을 제안하는 수표 발행 번호에 정상성 검사를 추가하였다. Dictionary 객체는 dictionary 내 주어진 키에 최근 무언가가 보관되어 있는지의 여부에 따라 boolean 을 이용해 includesKey: 메시지에 응답한다. 수표 발행 번호가 이미 사용되었다면 객체에 error: 메시지가 전송되고 연산이 취소된다.


마지막으로 dictionary에 새 엔트리를 추가하였다. 지침서 시작 부분에 at:put: (종종 앞에 샤프 기호를 붙여 #at:put: 으로 작성됨이 발견) 메시지를 이미 살펴본 바 있다. 여기서는 단순히 소비액과 수표 발행 번호를 연관시키기 위해 사용된다.[11] 이를 이용해 이제 합당한 정상성 검사와 수표별 정보로 작동하는 Checking 클래스를 갖게 되었다.


이 모든 정보로 접근하는 능력을 향상시킴으로써 이번 절을 마무리하고자 한다. 먼저 간단한 인쇄 함수부터 시작할 것이다.

printOn: stream [
    super printOn: stream.
    ', checks left: ' printOn: stream.
    checksleft printOn: stream.
    ', checks written: ' printOn: stream.
    (history size) printOn: stream.
]

check: num [
    | c |
    c := history
        at: num
        ifAbsent: [ ^self error: 'No such check #' ].
    ^c
]


몇 가지 놀라운 부분이 있을 것이다. 우리는 정보를 포맷팅하고 인쇄하는 동안 부모 클래스가 자신의 일을 처리하도록 두었다. 수표 발행 번호를 검색하면서 실행 가능한 문의 블록이 객체라는 사실을 다시 이용한다; 이번 경우 Dictionary 클래스에 의해 지원되는 at:ifAbsent: 메시지를 이용한다. 예상하는 바와 같이 요청된 키 값은 dictionary에서 발견되지 않으며 코드 블록이 실행된다. 이는 오류 처리를 개인설정 가능하게 해주는데, 일반적 오류는 사용자에게 'key not found(키가 발견되지 않았습니다)'라고만 말할 것이기 때문이다.


발행 번호를 알 경우 수표를 검색하면서 우리는 수표의 컬렉션을 '휙휙 넘겨보는' 방식을 작성하지 않았다. 아래 함수는 수표를 하나씩 처리하면서 행별로 하나씩 인쇄한다. 현재 각 키에는 하나의 수치 값만 있기 때문에 각 항목마다 공간을 남겨두는 것이 최상의 방법이다. 그리고 우리는 단순히 객체로 인쇄 메시지를 전송하는 것이므로 dictionary 내 객체가 printNl/printOn: 메시지에 응하는 한 우리는 다시 돌아와 이 코드를 재작성할 필요가 없을 것이다.

    printChecks [
        history keysAndValuesDo: [ :key :value |
            key print.
            ' - ' print.
            value printNl.
        ]
    ]
]


여전히 dictionary로 코드 블록이 전송되고 있지만 :key :value|는 새로운 것이다. 코드 블록은 선택적으로 인자를 수신할 수 있다. 이번 경우 두 개의 인자가 키/값 쌍을 표현한다. 값 부분만 원한다면 do: 메시지를 대신 이용해 내역(history)을 호출할 수 있고, 키 부분만 원한다면 keysDo: 메시지로 내역을 호출할 수 있을 것이다.


여기까지 완료되었다면 인쇄 인터페이스를 호출한다. 끝날 때까지 새로운 행은 원하지 않으므로 print 메시지가 대신 사용된다. printNl 과 비교할 때 둘 다 암묵적으로 Transcript를 사용한다는 점에서 상당 부분 비슷한데 print 메시지는 새 행을 추가하지 않는다는 점에서 차이가 있다.


코드 블록과 그 곳으로 전달한 dictionary 간에 관계가 없어야 한다는 원칙을 명확히 이해하는 것이 중요하다. Dictionary는 keysAndValuesDo: 메시지를 처리할 때 키/값 쌍을 이용해 전달된 코드 블록을 호출한다. 하지만 똑같은 평가를 원하는 어떤 메시지로든 2-매개변수 코드 블록을 전달할 수 있다 (그리고 매개변수의 정확한 수도 전달한다). 다음 절에서는 코드 블록이 어떻게 사용되는지 살펴보고, 자신의 코드에서 코드 블록을 호출하는 방법도 살펴볼 것이다.


코드 블록, 제 2부

마지막 6.7 절에서는 조건부 표현식의 빌드에 코드 블록이 어떻게 사용되는지를 살펴보고, 컬렉션 내 모든 엔트리를 반복하는 방식을 살펴보고자 한다.[12] 앞에서 우리는 고유의 코드 블록을 빌드하고 시스템 객체에 의해 사용되도록 하였다. 하지만 코드 블록의 호출에 대해서는 달리 마법이라 부를 만한 요소가 없었다; 당신의 코드가 주로 알아서 마법을 부려주기 때문이다. 이번 절에서는 스몰토크에서 루프 구성의 예를 몇 가지 살펴보고 나서 스스로 코드 블록을 호출하는 방식을 설명하겠다.

정수 루프

정수 루프는 루프를 이동시키기 위해 번호를 말해줌으로써 구성된다. 1부터 20까지 계수하는 아래 예제를 시도해보라:

1 to: 20 do: [:x | x printNl ]


1 이상의 숫자를 더하는 방식도 있다:

1 to: 20 by: 2 do: [:x | x printNl ]


마지막으로 내려세기는 음수 증가(negative step)를 이용한다:

20 to: 1 by: -1 do: [:x | x printNl ]


x 변수는 블록에 국부적(local)이라는 사실을 주목하라.

x


그러면 nil 이 인쇄된다.


Intervals

숫자 범위를 단일(standalone) 객체로 표현하는 것도 가능하다. 이는 숫자 범위를 시스템 주위로 전달 가능한 단일 객체로 표현하도록 해준다.

i := Interval from: 5 to: 10
i do: [:x | x printNl]


정수 루프와 마찬가지로 Interval 클래스 또한 1보다 큰 수의 증가(step)를 표현할 수 있다. 이는 위의 숫자형 루프와 비슷하게 이루어진다:

i := (Interval from: 5 to: 10 by: 2)
i do: [:x| x printNl]


코드 블록 호출하기

수표 예제를 다시 가져와 특정 금액 이상의 수표만 스캐닝하는 메서드를 추가해보도록 하자. 그러면 사용자는 그 아래 값은 건너뛰어 함수를 호출하지 않고 '큰' 수표를 찾을 수 있을 것이다. 수표 발행 번호를 인자로 하여 코드 블록을 호출할 것이다; 기존의 check: 메시지는 금액을 얻는 데 이용할 수 있다.

Checking extend [
    checksOver: amount do: aBlock
        history keysAndValuesDo: [:key :value |
            (value > amount)
                ifTrue: [aBlock value: key]
    ]
]


위 루프의 구조는 제 6장에 살펴본 printChecks 메시지와 많이 비슷하다. 하지만 이번 경우 우리는 각 엔트리를 고려하고, 수표 값이 명시된 값보다 큰 경우에만 제공된 블록을 호출한다.

ifTrue: [aBlock value: key]


위의 행은 사용자가 제공한 블록을 호출하면서 키, 즉 수표 발행 번호를 인자로서 전달한다. 코드 블록이 value: 메시지를 수신하면 이 메시지는 코드 블록의 실행을 야기한다. 코드 블록은 value, value:, value:value:, value:value:value: 메시지를 취하므로 0부터 3까지 인자를 코드 블록으로 전달할 수 있게 된다.[13]


연관관계(association)뿐만 아니라 코드 블록 또한 value 메시지를 취한다는 사실이 헷갈릴 수도 있겠다. 각 객체는 메시지를 이용해 고유의 일을 실행할 수 있음을 기억하라. 코드 블록은 value 메시지를 수신할 때 실행된다. 연관관계는 키/값 쌍에서 값 부분을 리턴할 뿐이다. 둘 다 동일한 메시지를 수신한다는 사실은 이번 사례에선 우연의 일치일 뿐이다.


새로운 수표 발행 계좌에 $250를 마련하여 (실제로 돈이 이렇게 쉽게 생긴다면 얼마나 좋을까?) 수표를 두 장 정도 사용해보자. 그러면 새 메서드가 올바르게 일을 실행함을 목격할 것이다:

mycheck := Checking new.
mycheck deposit: 250
mycheck newChecks: 100 count: 40
mycheck writeCheck: 10
mycheck writeCheck: 52
mycheck writeCheck: 15
mycheck checksOver: 1 do: [:x | x printNl]
mycheck checksOver: 17 do: [:x | x printNl]
mycheck checksOver: 200 do: [:x | x printNl]


checksOver: 코드를 작성하는 대안적 방법을 소개하면서 이번 절을 마무리하고자 한다. 본 예제에서는 값을 초과하는 수표를 고르기 위해 우리가 스스로 비교하는 대신 select: 메시지를 사용할 것이다. 이후 사용자의 코드 블록에 대해 새로운 결과 컬렉션을 호출할 것이다.

Checking extend [
    checksOver: amount do: aBlock [
        | chosen |
        chosen := history select: [:amt| amt > amount].
        chosen keysDo: aBlock
    ]
]


extend는 메서드를 덮어쓸 것이라는 사실도 주목하라. 위와 동일한 테스트를 시도하면 같은 결과를 낳을 것이다!


상황이 나빠질 경우

여기까지는 처음으로 작동하는 예제를 소개하였다. 올바로 타이핑하지 않으면 이해할 수 없는 경고가 넘쳐날 것이다. 경고를 무시하고 다시 입력하는 방법을 선택할 수도 있겠다.


하지만 자신만의 스몰토크 코드를 개발할 때 이러한 메시지들은 무엇이 잘못되었는지 알아내는 방식이 되기도 한다. 자신의 객체, 메서드, 오류 인쇄, 대화형 환경은 모두 동일한 스몰토크 세션 내부에 포함되어 있으므로 이러한 오류 메시지들을 이용해 매우 강력한 기법들을 사용하는 코드로 디버깅할 수 있다.

단순 오류

먼저 일반적 오류부터 살펴보자. 아래를 입력하라:

7 plus: 1


이는 아래를 인쇄할 것이다:

7 did not understand selector 'plus:'
<blah blah>
UndefinedObject>>#executeStatements


첫 행은 비교적 단순하다; 7 객체로 전송된 메시지가 이해되지 않았다는 의미다; plus: 연산은 + 이어야 하기 때문에 놀랄 일도 아니다. 그리고 이해할 수 없는 행도 있다: 무시해도 좋은데, 그 이유는 오류가 GNU Smalltalk의 예외 처리 시스템을 통과했다는 사실을 반영하기 때문이다. 나머지 행은 GNU Smalltalk에서 우리가 명령 프롬프트에 입력한 코드를 호출하는 방식을 반영한다; 따라서 Object 클래스에서 정의되었고 nil executeStatements 와 같이 평가된 내부 메서드 executeStatement 를 통해 호출되는 코드의 블록을 생성한다 (nil은 UndefinedObject 의 인스턴스다). 따라서 이 출력은 7 객체로 유효하지 않은 메시지를 전송하는 행을 직접 입력했음을 말해준다.


첫 행을 제외한 모든 오류 출력은 사실 스택 추적이다. 화면의 상단에 표시될수록 최신에 발생한 호출이다. 다음 예제에서는 객체 내부 더 깊은 곳에서 발생하는 오류를 야기할 것이다.


중첩 호출

아래 행을 입력하라:

x := Dictionary new
x at: 1


그러면 아래와 같은 오류를 얻게 될 것이다:

Dictionary new: 31 "<0x33788>" error: key not found
...blah blah...
Dictionary>>#error:
[] in Dictionary>>#at:
 [] in Dictionary>>#at:ifAbsent:
Dictionary(HashedCollection)>>#findIndex:ifAbsent:
Dictionary>>#at:ifAbsent:
Dictionary>>#at:
UndefinedObject(Object)>>#executeStatements


오류 자체는 꽤 분명하다; Dictionary 내부에 존재하지 않는 것을 요청한 것이다. 오류를 가진 객체는 Dictionary new: 31 로 식별된다. Dictionary의 기본 크기는 31이다; 따라서 이것이 우리가 Dictionary new를 이용해 생성한 객체다.


스택 추적은 Dictionary가 #at: 메시지에 어떻게 응답하는지에 대한 내부 구조를 보여준다. 우리가 직접 입력한 명령은 UndefinedObject(Object)에 대한 일반 엔트리를 야기한다. 그리고 Dictionary 객체가 #at: 메시지로 ('Dictionary >>#at:' 행) 응답함을 볼 수 있다. 이 코드는 #at:ifAbsent: 메시지를 이용해 객체를 호출하였다. 그러다 갑자기 Dictionary가 두 개의 블록을 평가하는 이상한 메서드 #findIndex:ifAbsent: 를 호출하자 오류가 발생한다.


이를 더 잘 이해하기 위해서는, 스몰토크에서는 오류가 발생 시 호출될 코드의 블록을 물려주는 것이 매우 일반적인 오류 처리 방식임을 아는 것이 중요하다. Dictionary 코드의 경우, 코드 블록에서 at: 메시지는 at:ifAbsent: 가 주어진 키를 찾을 수 없을 때 호출되는 at:ifAbsetn: 코드로 전달되는데, 이러한 행위는 at:ifAbsent: 와 findIndex:ifAbsent: 쌍에서도 발견된다. 따라서 Dictionary 자체에 대한 코드를 살펴보지 않고도 Dictionary의 구현에 대한 코드를 어느 정도 짐작할 수 있다:

findIndex: key ifAbsent: errCodeBlock [
    ...look for key...
    (keyNotFound) ifTrue: [ ^(errCodeBlock value) ]
    ...
]

at: key [
    ^self at: key ifAbsent: [^self error: 'key not found']
]


사실상 추적에서 Dictionary(HashedCollection)가 알려주듯 findIndex:ifAbsent: 는 HashedCollection 클래스에 위치한다.


스택 추적에서 모든 엔트리가 소스 행 숫자를 포함한다면 정말 좋을 것이다. 안타깝게도 현재 GNU Smalltalk는 이러한 기능을 제공하지 않는다. 물론 여기에 이용 가능한 소스 코드는 있지만 말이다...


객체 살펴보기

오류를 쫓다보면 자신의 객체의 인스턴스 변수를 조사하는 것이 도움이 되는 경우가 종종 있다. printNl 로의 전략적인 호출이 도움이 되는 것은 의심할 여지가 없지만 모든 코드를 작성할 필요 없이 객체를 살펴보도록 해보자. inspect 메시지는 어떤 객체에서든 작용하는데, 객체 내부에 각 인스턴스 변수의 값을 버린다.[14]


따라서 다음을 실행 시:

x := Interval from: 1 to: 5.
x inspect


아래 내용이 표시된다:

An instance of Interval
start: 1
stop: 5
step: 1
contents: [
    [1]: 1
    [2]: 2
    [3]: 3
    [4]: 4
    [5]: 5
]


이번 절은 앞에서 이미 다룬 기법을 강조하면서 마무리 짓겠다: 자신의 객체 내에서 error: 메시지를 사용하는 기법 말이다. Dictionary의 사례에서 보았듯 객체는 실행을 취소하고 스택 추적을 버리는 설명 문자열이 있는 error: 메시지를 스스로에게 전송한다. 자신의 객체에 이러한 기법을 사용하도록 하라. 사용자가 야기한 명시적 오류뿐만 아니라 내부 정상성 검사에도 사용할 수 있다.


클래스 계층구조에서 공존하기

본 지침서의 앞에 여러 장에 걸쳐서는 클래스를 두 가지 방식 중 하나로 논하였다. 앞서 발전시킨 '가상(toy)' 클래스는 Object에 기반을 둔다; 시스템이 제공하는 클래스는 변형이 불가한 개체(immutable entity)로 취급되었다. Ywns 클래스의 행위를 약간씩 수정할 수는 없지만 시스템이 제공한 기능들 중에는 올바른 장소에 자신의 클래스를 '플러그인'하면 약간의 수고로 매우 강력한 새 클래스를 제공할 수 있다.


이번 절은 기존 스몰토크 계층구조를 향상시키는 두 개의 완전한 클래스를 생성할 것이다. 먼저 새 클래스를 연결하는 장소 문제로 시작하여 구현으로 넘어갈 것이다. 대부분의 프로그래밍 시도와 마찬가지로 결과는 많은 개선의 가능성을 남길 것이다. 하지만 프레임워크는 자신만의 스몰토크 클래스를 개발하는 방식에 대한 직관력을 제공하기 시작할 것이다.


기존의 클래스 계층구조

새 클래스가 어디로 가는지 논하기 위해서는 현재 클래스의 맵(map)이 있다면 도움이 되겠다. 아래는 GNU Smalltalk의 기본 클래스 계층구조이다. 조금 덜 들여 쓴 앞 행은 다음에 따라오는 행으로 상속함을 의미한다.[15]

Object
    Behavior
        ClassDescription
            Class
            Metaclass
    BlockClosure
    Boolean
        False
        True
    Browser
    CFunctionDescriptor
    CObject
        CAggregate
            CArray
            CPtr
        CCompound
            CStruct
            CUnion
        CScalar
            CChar
            CDouble
            CFloat
            CInt
            CLong
            CShort
            CSmalltalk
            CString
            CUChar
                CByte
                CBoolean
            CUInt
            CULong
            CUShort
    Collection
        Bag
        MappedCollection
        SequenceableCollection
            ArrayedCollection
                Array
                ByteArray
                WordArray
                LargeArrayedCollection
                    LargeArray
                    LargeByteArray
                    LargeWordArray
                CompiledCode
                    CompiledMethod
                    CompiledBlock
                Interval
                CharacterArray
                    String
                        Symbol
            LinkedList
                Semaphore
            OrderedCollection
                RunArray
            SortedCollection
        HashedCollection
            Dictionary
                IdentityDictionary
                    MethodDictionary
                RootNamespace
                    Namespace
                    SystemDictionary
            Set
                IdentitySet
    ContextPart
        BlockContext
        MethodContext
    CType
        CArrayCType
        CPtrCType
        CScalarCType
    Delay
    DLD
    DumperProxy
        AlternativeObjectProxy
            NullProxy
                VersionableObjectProxy
            PluggableProxy
    File
        Directory
    FileSegment
    Link
        Process
        SymLink
    Magnitude
        Association
        Character
        Date
        LargeArraySubpart
        Number
            Float
            Fraction
            Integer
            LargeInteger
                LargeNegativeInteger
                LargePositiveInteger
                    LargeZeroInteger
            SmallInteger
        Time
    Memory
    Message
        DirectedMessage
    MethodInfo
    NullProxy
    PackageLoader
    Point
    ProcessorScheduler
    Rectangle
    SharedQueue
    Signal
        Exception
            Error
                Halt
                    ArithmeticError
                        ZeroDivide
                    MessageNotUnderstood
                UserBreak
            Notification
                Warning
    Stream
        ObjectDumper
        PositionableStream
            ReadStream
            WriteStream
                ReadWriteStream
                    ByteStream
                        FileStream
        Random
        TextCollector
        TokenStream
    TrappableEvent
        CoreException
        ExceptionCollection
    UndefinedObject
    ValueAdaptor
        NullValueHolder
        PluggableAdaptor
            DelayedAdaptor
        ValueHolder


처음엔 리스트가 너무 많아 버거울지 모르지만 시간을 들여 이번 지침서에서 살펴본 클래스를 하나씩 찾아보도록 해야 한다. 가령 Array가 어떻게 SequenceableCollection 클래스 아래에 있는 서브클래스가 되는지를 주목하라. 알고 보면 당연한 일이다; Array에서는 한쪽 끝에서 다른 끝으로 걸어갈 수 있다. 반대로 Sets는 왜 해당하지 않는지 주목하라: Set를 한쪽 끝부터 다른 끝으로 걷는다는 것은 말이 되지 않는다.


약간 당황스러운 것은 Bag와 Set의 관계인데, Bag는 사실 그 요소들의 다중 발생을 지원하는 Set이기 때문이다. 그 답은 Set와 Bag의 목적에서 찾아야 한다. 둘 다 정렬되지 않은 객체의 컬렉션을 보유한다; 하지만 Bag는 객체가 몇 천 번의 발생을 가질 수 있는 상황에 최적화되어 있는 반면 Set는 객체 유일성을 검사하는 데 최적화되어 있다. 그래서 Set가 Bag의 서브클래스이거나 Bag가 Set의 서브클래스가 되면 클래스의 실제 구현에서 문제의 근원이 될 수 있다. 현재 Bag는 각 객체를 각 계수로 연관시키는 Dictionary를 보유한다; 하지만 Bag를 HashedCollection의 서브클래스이자 Set의 자식으로 갖는 것은 가능하다.


숫자의 취급을 살펴보자-Magnitude 클래스부터 시작하여. 숫자는 사실 ~보다 적은, ~보다 큰 등의 순서로 정렬이 가능하므로 많은 다른 객체들도 마찬가지로 이러한 순서로 정렬이 가능하다. Magnitude 의 각 서브클래스는 그러한 객체이다. 따라서 문자끼리 비교하고, 일자를 다른 일자와 비교하며, 시간끼리, 숫자끼리 비교가 가능하다.


마지막으로, 당신이 절대 객체로 생각하지 않았던 언어 개체를 표현하는 꽤 이상한 클래스를 몇 가지 눈치챘을 것이다: Namespace, Class, 심지어 CompiledMethod 까지. 이들은 Smalltalk의 '반영적(reflection)' 메커니즘의 기반으로, 122 페이지의 6.12.3절 [메타클래스에 관한 사실]에서 논할 것이다.


Arrays 갖고 놀기

배열이 필요한데 색인이 범위(bound)를 벗어난 경우 nil을 리턴해야 한다고 가정하자. 스몰토크 구현을 수정할 수는 있지만 이미지에서 일부 코드를 망가뜨릴 수 있으므로 그다지 실용적이지 못하다. 그렇다면 왜 서브클래스를 추가하지 않는 걸까?

"We could subclass from Array, but that class is specifically optimized by the VM (which assumes, among other things, that it does not have any instance variables). So we use its abstract superclass instead. The discussion below holds equally well."
"Array에서 서브클래싱을 할 수도 있지만 그 클래스는 VM(무엇보다 어떤 인스턴스 변수도 갖지 않는다고 가정한다)에 의해 특별히 최적화되었다. 따라서 그것의 추상적 슈퍼클래스를 대신 사용한다. 아래 논의도 똑같이 유효하다."

ArrayedCollection subclass: NiledArray [
    <shape: #pointer>
    
    boundsCheck: index [
    ^(index < 1) | (inde    x > (self basicSize))
    ]
    
    at: index [
        ^(self boundsCheck: index)
            ifTrue: [ nil ]
            ifFalse: [ super at: index ]
    ]
    at: index put: val [
        ^(self boundsCheck: index)
            ifTrue: [ val ]
            ifFalse: [ super at: index put: val ]
    ]
]


클래스를 추가하는 기술(machinery) 대다수는 익숙할 것이다. 그 외에 shape: 메시지인 comment: 와 같은 선언도 눈에 띈다. 이는 NiledArray가 Array 객체와 동일한 기본 구조를 갖도록 준비시킨다; 이와 관련된 논의는 배열에 대한 기초적 사실을 다룬 장으로 미루겠다. 어떤 경우든 우리는 배열을 어떻게 생성하는지와 어떻게 참조하는지 등의 실제 지식을 모두 상속한다. 우리가 하는 일은 at: 과 at:put: 메시지를 방해하고, 배열 색인을 검증하기 위해 공통된 함수를 호출하며, 색인이 유효하지 않은 경우 특별한 조치를 취하는 일이다. 범위 검사(bounds check)를 코딩하는 방식은 좀 더 살펴볼 필요가 있겠다.


범위 검사의 코딩을 처음으로 작업하면서 NiledArray의 메서드에서 범위를 두 번 코딩했을 것이다 (한 번은 at:, 두 번째는 at:put: 을 대상으로). 언제나처럼 코딩은 한 번만 실행한 후 재사용하는 편이 선호된다. 따라서 범위 검사를 위한 메서드 boundsCheck: 를 추가하여 두 사례에서 사용하였다. 범위 검사를 향상시키길 원한다면 (아마도 색인이 <1인 경우 오류를 발생시키고, 색인이 배열 크기보다 클 때만 nil을 응답할 것), 한 곳에서만 변경하면 될 일이다.


범위를 벗어났는지 계산하는 실제 수학 문제가 흥미로운 부분이다. 메서드가 리턴한 표현식의 첫 번째 부분은 다음과 같다:

(index < 1) | (index > (self basicSize))


이는 색인이 만일 1보다 작으면 true이고, 그 외는 false가 된다. 따라서 이 표현식 부분은 부울 객체의 true 또는 false가 된다. 부울 객체는 이후 메시지 |와 (index > (self basicSize)) 인자를 수신한다. | 는 'or(택 1)'을 의미한다-범위를 벗어난 수표 두 개를 OR(택 1) 하고자 한다. 표현식의 두 번째 부분은 무엇일까?[16]


index 는 우리의 인수, 정수이다; 이는 > 메시지를 수신하고, 그에 따라 self basicSize가 리턴하는 값에 자신의 값을 비교한다. 스몰토크가 배열을 빌드하기 위해 사용하는 기본 구조를 다루지는 않았지만 #basicSize 메시지는 Array 객체가 포함할 수 있는 요소의 수를 리턴한다고 간략하게 말할 수 있겠다. 따라서 색인이 1(Array 색인에 허용되는 최저 숫자)보다 작은지 혹은 Array에 할당된 가장 높은 수의 슬롯보다 큰지 검사된다. 둘 중에 (| 연산자!) 하나일 경우 true이고, 나머지는 false이다.


여기서부터 힘들어진다; boundsCheck: 가 리턴한 부울 객체가 ifTrue:ifFalse: 메시지와 코드 블록을 수신하여 적절한 작업을 실행할 것이다. 그렇다면 at:put: 리턴 값은 왜 갖는가? 왜냐면 본래 그래야 하기 때문이다: at:put 또는 at: 의 모든 구현자를 살펴보면 두 번째 매개변수를 리턴함을 발견할 것이다. 일반적으로 결과는 폐기된다; 하지만 결과를 사용하는 프로그램을 작성할 수도 있는데 본문에서도 이런 방식으로 작성할 것이다.


새로운 유형이 숫자 추가하기

복잡한 수학이 많은 애플리케이션을 프로그래밍하고 있다면 수많은 2-소자(two-element) 배열을 이용해 관리하고 있을 것이다. 하지만 수학과 비교식을 위한 인라인 코드를 평생 작성해야 할 것이다; 오히려 복잡한 숫자 타입을 지원하는 객체 클래스를 구현하는 편이 훨씬 수월할 것이다. 클래스 계층구조에서 어디에 위치해야 할까?


이미 짐작한 사람이 있을 것이지만 어쨌든 계층구조를 살펴보자. 모든 것은 Object로부터 상속되므로 시작하기에 안전한 장소가 되겠다. 복소수는 < and >로 비교할 수 없지만 그래도 숫자기 때문에 Number 클래스에 속해야 한다고 강하게 믿고 싶을 것이다. 하지만 Number는 바로 Magnitude 로부터 상속된다-이런 갈등을 어떻게 해소할 수 있을까? 서브클래스는 자신이 허용하고 싶지 않은 일부 연산을 허용해주는 슈퍼클래스 아래로 스스로를 위치시킬 수 있다. 당신이 해야 할 일은 이러한 메시지들을 방해하여 오류를 리턴하도록 만들도록 확보하기만 하면 된다. 따라서 새 Complex 클래스를 Number 아래에 위치시켜 비교 연산자를 허용하지 않도록 할 것이다.


복소수의 실제 부분과 가상 부분이 정수가 될 것인지 부동 소수점이 될 것인지 질문하고 싶은 사람이 분명히 있을 것이다. 스몰토크의 관습에 따라 이들은 그저 객체로 남겨두고, 숫자 메시지에 적당하게 응답하길 바랄 것이다. 응답하지 않는다면 사용자는 틀림없이 오류를 수신하고, 약간의 수고를 들여 자신의 실수를 다시 추적해갈 수 있을 것이다.


네 개의 기본 수학 연산자를 비롯해 (유효하지 않은) 관계형(relationals)을 정의할 것이다. printOn:을 추가하여 인쇄 메서드가 작동하도록 하였으므로 우리의 Complex 클래스도 제공할 것이다. 제시된 바와 같이 클래스에는 몇 가지 한계점이 있는데, 이번 절 뒷부분에서 다루도록 하겠다.

Number subclass: Complex [
    | realpart imagpart |
    
    "This is a quick way to define class-side methods."
    Complex class >> new [
        <category: 'instance creation'>
        ^self error: 'use real:imaginary:'
    ]
    
    Complex class >> new: ignore [
        <category: 'instance creation'>
        ^self new
    ]
    
    Complex class >> real: r imaginary: i [
        <category: 'instance creation'>
        ^(super new) setReal: r setImag: i
    ]
    
    setReal: r setImag: i [
        <category: 'basic'>
        realpart := r.
        imagpart := i.
        ^self
    ]
    
    real [
        <category: 'basic'>
        ^realpart
    ]
    
    imaginary [
        <category: 'basic'>
        ^imagpart
    ]
    
    + val [
        <category: 'math'>
        ^Complex real: (realpart + val real)
            imaginary: (imagpart + val imaginary)
    ]
    
    - val [
        <category: 'math'>
        ^Complex real: (realpart - val real)
            imaginary: (imagpart - val imaginary)
    ]
    
    * val [
        <category: 'math'>
        ^Complex real: (realpart * val real) - (imagpart * val imaginary)
            imaginary: (imagpart * val real) + (realpart * val imaginary)
    ]

    / val [
        <category: 'math'>
        | d r i |
        d := (val real * val real) + (val imaginary * val imaginary).
        r := ((realpart * val real) + (imagpart * val imaginary)).
        i := ((imagpart * val real) - (realpart * val imaginary)).
        ^Complex real: r / d imaginary: i / d
    ]
    
    = val [
        <category: 'comparison'>
        ^(realpart = val real) & (imagpart = val imaginary)
    ]
    
    "All other comparison methods are based on <"
    < val [
        <category: 'comparison'>
        ^self shouldNotImplement
    ]
    
    printOn: aStream [
        <category: 'printing'>
        realpart printOn: aStream.
        aStream nextPut: $+.
        imagpart printOn: aStream.
        aStream nextPut: $i
    ]
]


놀라울 정도로 적은 내용인데 이번 예제에선 아마 처음 있는 일일 것이다. 인쇄 메서드는 printOn: 뿐만 아니라 nextPut: 을 이용해 인쇄를 실행한다. 여기선 다루지 않았지만 $+ 는 ASCII 문자 + 를 객체로서[17] 생성하고, nextPut: 은 스트림 상에서 그 인자를 다음 대상으로 놓는다.


수학 연산은 모두 새 객체를 생성하여 실제 및 가상 부분을 계산하고, 새 객체를 생성하기 위해 Complex 클래스를 호출한다. 본문의 생성 코드는 앞의 예제들에 비해 좀 더 간단하다; 새로 생성된 객체를 명명하기 위해 지역 변수를 사용하는 대신 리턴 값을 사용하고 새 객체로 직접 메시지를 전송하는 편을 택했다. 초기화 코드는 명시적으로 self를 리턴한다; 이것을 멈추면 어떤 일이 발생할까?


상속과 다형성

앞의 두 예제로 했던 일을 좀 더 높은 수준에서 살펴보면 좋은 때가 왔다. NiledArray 클래스를 이용해 배열의 거의 모든 기능을 상속하였고, 특정 요구를 강조하기 위해 약간의 코드만 추가하였다. 시도조차 안 해봤을지도 모르지만 Array에 대한 모든 기존 메서드는 추가적 노력 없이도 계속 작동한다-아래와 같은 행이 여전히 작동하는 이유를 생각하면 재미있을 것이다:

a := NiledArray new: 10
a at: 5 put: 1234
a do: [:i| i printNl ]


상속의 강점은 당신이 실행하는 증분 변경에 초점을 둔다는 점이다; 변경하지 않은 내용은 대체로 계속해서 작동할 것이다.


Complex 클래스에서는 다형성의 가치가 발휘된다. Complex 숫자는 다른 숫자와 정확히 동일한 메시지 집합에 응답한다. 이 코드를 다른 사람에게 전달한다면 별도의 지시 없이도 Complex 숫자로 어떻게 수학문제를 푸는지 알 것이다. 복소수 패키지에서 사용자가 complex-add 함수가 complex_plus()인지, complex_add()인지, 아니면 add_complex()인지 등을 알아내야 하는 C와 비교해보라.


하지만 Complex 클래스에는 한 가지 중요한 사항이 빠져 있다: Complex 숫자를 일반 숫자와 섞으면 어떤 일이 발생할까? 현재 Complex 클래스는 그것이 다른 Complex 숫자와만 상호작용할 것이라고 가정한다. 하지만 이는 비현실적이다: 수학적으로 '일반' 숫자는 0의 가상 부분이 있는 단순한 숫자이다. 스몰토크는 숫자가 다른 숫자와 작용하는 형태가 되도록 강요하도록 설계되었다.


시스템은 현명하며 추가 코드를 거의 필요로 하지 않는다. 안타깝게도 필요한 설명은 세 배로 증가할 것이다. GNU Smalltalk 에선 강요가 어떻게 이루어지는지 궁금하다면 스몰토크 라이브러리 소스를 확인하고 retry:coercing: 메시지의 실행을 추적해보길 바란다. generality 메시지가 각 숫자 타입에 리턴하는 값을 고려하길 원할 것이다. 마지막으로 각 숫자 클래스에서 coerce: 처리를 살펴볼 필요가 있다.


스몰토크 스트림

아직 논하진 않았지만 우리 예제들은 메커니즘을 광범위하게 사용하였다. Stream 클래스는 입출력 기능, 큐, 동적으로 생성된 데이터의 무한한 소스를 포함해 수많은 데이터 구조를 위한 프레임워크를 제공한다. 스몰토크 스트림은 C에서 사용한 UNIX 스트림과 꽤 비슷하다. 스트림은 기반이 되는 자원으로 연속적 뷰(sequential view)를 제공한다; 따라서 요소를 읽거나 쓰면 기본 매체의 끝에 달할 때까지 스트림 위치가 나아간다. 대부분의 스트림은 현재 위치로 설정하도록 허용하고, 매체로 랜덤 액세스를 제공한다.

출력 스트림

본 저서에 제시된 예제들은 모두 출력을 Transcript 스트림으로 작성하기 때문에 작동한다. 각 클래스는 printOn: 메서드를 구현하는데, 이는 제공된 스트림으로 그 출력을 작성한다. 모든 객체가 사용하는 printNl 메서드는 단순히 현재 객체로 Transcript를 인자로 한 printOn: 메시지를 전송한다 (기본적으로 stdout 전역 변수에서 찾을 수 있는 표준 출력 스트림에 붙어 있음). 표준 출력 스트림은 직접 호출하거나:

'Hello, world' printOn: stdout
stdout inspect


또 다른 스트림이긴 하지만 Transcript에도 동일하게 적용할 수 있다:


'Hello, world' printOn: stdout Transcript inspect


마지막 inspect 문은 Transcript가 어떻게 stdout로 연결되어 있는지를 보여줄 것이다.[18]


자신만의 스트림

C에서 생성할 수 있는 파이프(pipe)와 달리 Stream의 저장공간은 당신의 통제 하에 있다. 따라서 Stream은 익명 데이터 버퍼를 제공하는데, 기존 데이터 배열로 스트림과 같은 해석을 제공하기도 한다. 아래 예제를 고려해보라:

a := Array new: 10
a at: 4 put: 1234
a at: 9 put: 5678
s := ReadWriteStream on: a.
s inspect
s position: 1
s inspect
s nextPut: 11; nextPut: 22
(a at: 1) printNl
a do: [:x| x printNl]
s position: 2
s do: [:x| x printNl]
s position: 5
s do: [:x| x printNl]
s inspect


키는 on: 메시지에 위치한다; 이는 스트림 클래스에게 기존 저장공간과 관련하여 스스로를 생성하라고 말한다. 다형성으로 인해 on: 에 의해 명시된 객체는 Array일 필요가 없다; 숫자 at: 메시지에 응답하는 어떤 객체든 이용할 수 있다. 앞 절에서 연습한 NiledArray 클래스가 아직도 로딩되어 있다면 그러한 유형의 배열에 스트리밍을 시도해보아도 좋겠다.


스트림을 생성하는 당시 Stream에 얼마나 많은 데이터가 대기(queued)할 것인지 알아야 하는지 여부도 궁금할 것이다. 올바른 스트림의 클래스를 사용하고 있다면 답은 '아니오'다. ReadStream은 기존 컬렉션으로 읽기만 가능한 접근성을 제공한다. 만일 무언가 쓰려고 한다면 오류를 수신할 것이다. 스트림 끝을 읽어내려고 한다면 오류를 받을 것이다.


반면 (예제에서 사용된) WriteStream과 ReadWriteStream은 기본 컬렉션에게 사용자가 기존 컬렉션의 끝을 읽으려고 시도하면 증가하도록 시킨다. 따라서 여러 개의 문자열을 작성하고 싶고, 그 길이를 스스로 더하고 싶지 않다면:

s := ReadWriteStream on: String new
s inspect
s nextPutAll: 'Hello, '
s inspect
s nextPutAll: 'world'
s inspect
s position: 1
s inspect
s do: [:c | stdout nextPut: c ]
s contents


이번에는 Stream에 대한 컬렉션으로 String을 사용해 보았다. printOn: 메시지는 처음에 빈 문자열로 바이트를 추가한다. 데이터를 추가하고 나면 데이터를 계속해서 스트림으로 취급할 수 있다. 이 방법이 아니라면 기본 객체를 사용자에게 리턴해줄 것을 스트림으로 요청할 수도 있다. 그러면 고유의 접근 메서드를 이용하는 객체를 (이번 예제에서 String) 이용할 수 있을 것이다.


스트림 객체에는 이용할 수 있는 편리한 기능이 많다. attend 로 읽을 것이 더 있는지 물어볼 수 있다. position을 이용하면 위치를 문의하고, position: 을 이용하면 위치를 설정할 수 있다. peek 는 다음으로 무엇을 읽을 것인지 볼 수 있고, 다음 요소는 next를 이용해 읽을 수 있다.


쓰기 방향에서는 nextPut: 를 이용해 요소를 작성할 수 있다. 당신의 스트림을 목적지로 한 printOn:을 실행하는 객체를 염려하지 않아도 된다; 해당 연산은 당신의 스트림에 nextPut: 연산의 시퀀스로 끝이 난다. 작성해야 할 것들의 컬렉션을 갖고 있다면 컬렉션을 인자로 한 nextPutAll: 을 이용할 수 있다; 컬렉션의 각 멤버는 스트림 상에 작성될 것이다. 스트림으로 객체를 여러 번 작성할 경우 아래와 같이 next:put: 을 사용할 수 있다:

s := ReadWriteStream on: (Array new: 0)
s next: 4 put: 'Hi!'
s position: 1
s do: [:x | x printNl]


파일

스트림은 파일 상에서도 작동이 가능하다. 단말기로 /etc/pass 파일을 버리고 싶다면 파일 상에 스트림을 생성한 후 그 내용을 스트리밍(stream over)하면 된다:

f := FileStream open: '/etc/passwd' mode: FileStream read
f linesDo: [ :c | Transcript nextPutAll: c; nl ]
f position: 30
25 timesRepeat: [ Transcript nextPut: (f next) ]
f close


그리고 물론 스몰토크 소스 코드를 이미지로 로딩할 수도 있다:

FileStream fileIn: '/Users/myself/src/source.st'


동적 문자열

스트림은 수많은 데이터 구조에 강력한 추상화를 제공한다. 현재 위치, 다음 위치 작성하기, 편의를 위해 데이터 구조를 보는 방식 변경하기 등의 개념을 결합 시 작지만 강력한 코드를 작성하도록 해준다. 실제 스몰토크 소스 코드에서 발췌한 내용을 마지막 예제로 들고자 한다-이는 객체가 스스로를 문자열로 인쇄하도록 만드는 일반 메서드를 보여준다.

printString [
    | stream |
    stream := WriteStream on: (String new).
    self printOn: stream.
    ^stream contents
]


Object에 상주하는 위의 메서드는 스몰토크 내 모든 클래스에 의해 상속된다. 첫 행은 현재 길이가 0인 String에 저장하는 WriteStream을 생성한다 (String new는 단순히 빈 문자열을 생성한다). 이후 printOn: 으로 현재 객체를 호출한다. 객체가 스스로를 '스트림'으로 인쇄하면서 String은 증가하여 새 문자를 수용한다. 객체가 인쇄를 완료하면 메서드는 단순히 기본 문자열을 리턴한다.


코드를 작성하면서 printOn: 이 단말기로 갈 것이라고 가정해왔다. 하지만 /dev/tty 와 같은 파일에 대한 스트림을 데이터 구조(String new)에 대한 스트림으로 대체하더라도 똑같이 잘 작동한다. 마지막 행은 Stream에게 기본 컬렉션을 리턴하도록 알리는데, 이는 모든 인쇄가 추가된 문자열이 될 것이다. 그 결과 printString 메시지는 메시지를 수신하는 바로 그 객체의 인쇄된 표현을 내용으로 가진 String 클래스의 객체를 리턴한다.



스몰토크에서 예외 처리

지침서에서 여기까지는 오리지널 Smalltlk-80 오류 신호방식을 사용했다:

check: num [
    | c |
    c := history
        at: num
        ifAbsent: [ ^self error: 'No such check #' ].
    ^c
]


위 코드에서 일치하는 수표 발행 번호가 발견되면 메서드는 그와 연관된 객체로 응답할 것이다. 일치하는 내용이 발견되지 않으면 스몰토크는 스택을 언와인드(unwind)하여 당신이 제공한 메시지와 스택 정보를 포함해 오류 메시지를 출력한다.

CheckingAccount new: 31 "<0x33788>" error: No such check #
...blah blah...
CheckingAccount>>#error:
[] in Dictionary>>#at:ifAbsent:
Dictionary(HashedCollection)>>#findIndex:ifAbsent:
Dictionary>>#at:ifAbsent:
[] in CheckingAccount>>#check:
CheckingAccount>>#check:
UndefinedObject(Object)>>#executeStatements


  1. error: 메시지를 수신한 객체, 메시지 텍스트 자체, 시스템이 오류를 포착 시 (가장 내부에서제일 먼저) 실행되는 프레임이 눈에 띈다. 또 CheckingAccount>>#check: 와 같은 메서드에서 나머지 코드 부분은 실행되지 않았다.


따라서 단순한 오류 보고는 우리가 원하는 기능 대부분을 제공한다:

  • 실행이 즉시 중단되어 프로그램이 아무 것도 잘못되지 않은 것처럼 계속되는 일을 방지한다.
  • 실패한 코드는 다소 유용한 오류 메시지를 제공한다.
  • 진단에 기본 시스템 상태 정보가 제공된다.
  • 디버거는 상태로 주입되어 수신자나 인자의 세부 내용과 같은 정보를 스택에 제공한다.


하지만 이보다 좀 더 강력하고 복잡한 오류 처리 메커니즘이 있는데, 예외라고 불린다. 이는 다른 프로그래밍 언어에서의 '예외'와 비슷하지만 보다 더 강력하고, 항상 오류 상태를 나타내는 것은 아니다. 예외와 관련해 '신호(signal)'이란 용어를 종종 사용하긴 하지만 일부 운영체제에서 제공하는 SIGTERM이나 SIGINT와 같은 신호와 혼동하지 말길 바란다; 이 둘은 다른 개념이다.


  1. error: 대신 예외를 사용하기로 결정하는 이유는 미적인 문제 때문이지만 단순한 규칙을 사용해도 좋다: 호출자에게 특정 오류로부터 현명하게 회복하는 방법을 호출자에게 제공할 경우나, 그러한 특정 오류를 신호로 보내기 위해서만 예외를 사용한다.


예를 들어, 워드 프로세서를 작성 중이라면 사용자에게 읽기만 가능한(read-only) 텍스트 영역을 만들 수 있는 방법을 제공할 수 있겠다. 그리고 사용자가 텍스트의 편집을 시도하면 읽기만 가능한 텍스트를 모델링하는 객체는 ReadOnlyText 혹은 다른 유형의 예외를 신호로 보내어 사용자 인터페이스 코드는 예외 언와인드를 중단시키고 사용자에게 오류를 보고할 수 있다.


예외가 유용한지 아닌지 의심이 간다면 간단하게 #error: 를 대신 사용하여 실험해보라. #error: 를 명시적 예외로 변환하는 편이 그 반대보다 훨씬 수월하다.


예외 생성하기

GNU Smalltalk는 몇 가지 예외를 제공하는데, 이 예외들은 모두 Exception의 서브클래스이다. 당신이 생성할 법한 것은 SystemExceptions 네임스페이스에 있다. 기반 라이브러리 참조에서는 내장된 예외를 살펴볼 수 있고, 그 이름은 Exception printHierarchy을 이용해 볼 수 있다.


몇 가지 유용한 시스템 예외로 의미가 분명해야 하는 SystemExceptions.InvalidValue, 아래에서 살펴볼 SystemExceptions.WrongMessageSent가 있다.


새 인스턴스를 생성하기 위한 #new를 더 이상 지원하지 않도록 클래스 중 하나를 변경한다고 치자. 그렇더라도 스몰토크의 일급 클래스 기능을 사용하기 때문에 모든 send를 찾고 변경하기가 쉽지 않다. 이제 아래와 같은 내용을 실행할 수 있겠다:

Object subclass: Toaster [
    Toaster class >> new [
        ^SystemExceptions.WrongMessageSent
            signalOn: #new useInstead: #toast:
    ]

    Toaster class >> toast: reason [
        ^super new reason: reason; yourself
    ]

    ...
]


인정하건대 위는 예외를 사용하기 위한 조건에 그다지 일치하지 않는다. 하지만 예외 타입이 이미 제공되기 때문에 이러한 유형의 방어적 프로그래밍을 하고 있다면 #error: 대신 이 편이 수월할지 모른다.


예외 발생시키기

예외 발생은 사실상 2 단계 과정이다. 첫째, 예외 객체를 생성하라; 이후 그 객체로 #signal을 전송하라.


계층구조를 살펴보면 편의상 이 두 단계를 하나로 결합하는 클래스 메서드가 많다는 사실을 발견할 것이다. 가령, Exception 클래스는 #new 와 #signal 을 제공하는데, 후자는 단순히 ^self new signal이다.


자신만의 예외 생성 메서드를 시그널링하는 변형체(signaling variant)만 제공하는 건 어떨까 생각할지도 모르겠다. 하지만 그럴 경우 당신의 서브클래스가 새 인스턴스 생성 메서드를 평범하게 제공할 수 없게 되는 문제를 야기할 것이다.

Error subclass: ReadOnlyText [
    ReadOnlyText class >> signalOn: aText range: anInterval [
        ^self new initText: aText range: anInterval; signal
    ]

    initText: aText range: anInterval [
        &lt;category: private>
        ...
    ]
]


여기서 ReadOnlyText를 서브클래싱하고 새로운 정보를 시그널링하기 전에 인스턴스로 추가하고 싶다면 private 메서드 #initText:range: 를 사용해야 할 것이다.


새 코드에서 인스턴스 생성 변형(variant)의 시그널링은 누락하길 권하는데, 수고를 미비하게 덜어주고 시그널링 코드의 명확성이 줄어들기 때문이다. 변형(variant)의 시그널링을 언제 포함할지는 상황을 평가한 후 자신의 판단에 맡기도록 한다.


예외 처리하기

특정 코드 블록에서 예외가 발생할 때 그것을 처리하기 위해서는 아래와 같이 #on:do: 를 이용하라:

^[someText add: inputChar beforeIndex: i]
    on: ReadOnlyText
    do: [:sig | sig return: nil]


이 코드는 첫 번째 블록이 실행되는 동안 ReadOnlyText 신호에 대한 처리기를 처리기 스택에 놓을 것이다. 그러한 예외가 발생하였으나 스택에서 시그널링 포인트에 가까운 처리기가 예외를 처리하지 않을 경우 ('내부 처리기'라고 알려짐) 예외 객체는 스스로를 do: 인자로서 처리기 블록에 전달할 것이다.


거의 대부분의 경우 당신은 이 객체가 예외를 처리할 수 있길 원할 것이다. 기본 처리기 액션에 여섯 개가 있는데 모두 예외 객체로 메시지로서 전송된다:


return: 해당 #on:do: 를 수신한 블록을 종료하면서 주어진 값을 리턴한다. #return 을 전송함으로써 인자를 누락시킬 수도 있는데, 이런 경우 nil이 될 것이다. 이 처리기가 당신이 제공할지 모르는 값으로 예외를 처리하길 원한다면 대신 블록과 함께 #retryUsing: 을 이용해야 한다.


retry 첫 번째 블록을 재시작함으로써 'goto'와 같이 행동한다. 분명한 것은 예외를 발생한 상황을 수정하지 않을 경우 무한 루프로 이어질 수 있다는 사실이다.


  1. retry는 스택 높이를 증가시키지 않기 때문에 회복 후 재호출을 구현하는 훌륭한 방법이다. 가령:
frobnicate: n [
    ^[do some stuff with n]
        on: SomeError
        do: [:sig | sig return: (self frobnicate: n + 1)]
]


위의 내용은 retry로 대체되어야 한다:

frobnicate: aNumber [
    | n |
    n := aNumber.
    ^[do some stuff with n]
        on: SomeError
        do: [:sig | n := 1 + n. sig retry]
]


retryUsing: #retry와 같지만 본래 블록을 인자로서 주어진 블록으로 효과적으로 대체한다는 점에서 다르다.


pass 예외에게 외부 처리기가 예외를 처리하도록 둘 것을 알리고 싶다면 #signal 대신 #pass를 사용하라. 다른 언어에서는 잡힌 예외를 다시 throw하는 것과 같다.


resume: 매우 흥미로운 메서드다. 스택을 언와인드하는 대신 #signal 전송으로부터 효과적으로 인자를 응답할 것이다. 다시시작 가능(resumable) 예외로 #signal 을 전송하는 코드는 이 값을 사용하거나 무시하거나 실행을 계속할 수 있다. 인자를 누락시킬 수도 있는데, 이런 경우 #signal 전송 시 nil을 응답할 것이다. 다시시작 가능한 예외가 되고 싶은 예외는 #isResumable 메서드로부터 true를 응답함으로써 이 기능을 등록해야 하는데, 이는 #resume: 전송 시마다 검사된다.


outer #pass와 거의 동일하지만 외부 처리기가 #resume: 을 사용 시 애초에 #signal 을 전송한 코드 조각이 아니라 이 처리기 블록이 재개된다 (그리고 #outer는 #resume: 에게 주어진 인자를 응답할 것이다).


해당 메서드 중에 호출하는 처리기 블록으로 리턴하는 메서드는 #outer밖에 없으며, 그 중에서도 특정 사례만 설명하였다.


예외는 그 외에도 여러 기능을 제공한다; 이러한 기능을 이용해 할 수 있는 다양한 일들은 Signal과 Exception 클래스 상의 메서드 살펴보라. 다행히 위의 메서드는 대부분의 경우 당신이 원하는 일을 해줄 수 있다.


이러한 메서드나 다른 예외 기능을 이용해 처리기를 종료하고 싶지 않다면 스몰토크는 처리기 블록에서 무엇을 응답하든 당신이 sig return: 을 하려던 것으로 가정할 것이다. 이에 의존하지 않길 권한다; 대신 명시적 sig return:을 사용해야 한다.


다중 예외 타입을 처리하는 빠르고 간단한 방법은 ExceptionSet를 이용하는 방법으로, 클래스 union의 예외에 대해 단일 처리기를 갖도록 허용한다:

^[do some stuff with n]
    on: SomeError, ReadOnlyError
    do: [:sig | ...]


이 코드에서 SomeError 또는 ReadOnlyError 신호는 모두 주어진 처리기 블록에 의해 처리될 것이다.


예외가 처리되지 않을 때

모든 예외는 처리기가 발견되지 않을 때 기본적으로 위의 처리기 액션 중 하나를 선택하거나, 모두 #pass를 이용한다. 이는 클래스에 #defaultAction 을 전송함으로써 호출된다.


기본 액션의 예는 위에서 #error: 의 사용 예로 제시하였다; 그 기본 액션은 메시지의 인쇄, 추적, 스택 언와인드가 해당한다.


자신만의 예외 클래스에 대한 기본 액션을 선택하는 가장 수월한 방식은 다음 절에서 설명하는 바와 같이, 이미 올바른 것을 선택한 예외 클래스로부터 서브클래싱하는 방법이다. 예를 들어, 경고와 같은 일부 예제들은 기본적으로 다시시작(resume by default)되므로 거의 대부분 다시시작될 것으로 취급되어야 한다.


슈퍼클래스에 따른 선택은 결코 의무가 아니다. Error 서브클래스를 다시시작 가능(resumable)하도록 특수화하거나, 심지어 자신의 설계에 맞다면 기본적으로 다시시작되는 것도 전적으로 수락된다.


새 예외 클래스 생성하기

코드가 신호를 받은(signaled) 예외를 처리할 수 있길 원한다면 그러한 유형을 자동으로 선택하는 방법을 제공하길 원할 것이다. 가장 쉬운 방법은 Exception을 서브클래싱하는 것이다.


가장 먼저 특수화할 예외 클래스를 선택해야 한다. Error 는 다시시작이 불가한(non-resumable) 예외에 가장 나은 선택이고, Notification 또는 그 서브클래스 Warning은 기본적으로 nil로 다시시작되어야 하는 예외에 최선의 선택이다.


예외는 일반 객체일 뿐이다; 처리기에 유용하다고 생각되는 정보는 모두 포함시켜라. 텍스트 설명 필드에는 두 가지가 있는데, 설명(description)과 메시지 텍스트가 그것이다. 설명이 제공될 경우에는 #description 상에 오버라이드 메서드로부터 응답한 상수의 문자열로 대부분 구성되고, 예외 클래스의 모든 인스턴스를 설명해야 한다. 메시지 텍스트는 시그널링 시점에서 제공되어야 하며, 코드가 제공했으면 하는 추가 정보에 사용되어야 한다. 시그널링 코드는 #signal 대신 #signal: 을 이용함으로써 messageText를 제공할 수 있다. 이러한 점은 인스턴스 생성 메시지의 변형(variant)에 대한 시그널링이 그 장점에 비해 수고가 많이 드는 또 다른 이유가 되기도 한다.



스택 언와인딩 끌어들이기

  1. on:do: 보다 더 자주 유용한 것으로 #ensure: 가 있는데, 이는 일반 실행 때문이든 신호를 받은 예외 때문이든 스택이 언와인드할 때 일부 코드가 실행되도록 보장한다.


  1. ensure:의 예제를 비롯해 신호가 없이도 스택이 언와인드할 수 있는 상황의 예제를 소개하겠다:
Object subclass: ExecuteWithBreak [
    | breakBlock |

    break: anObject [
        breakBlock value: anObject
    ]

    valueWithBreak: aBlock [
        "Sets up breakBlock before entering the block,
        and passes self to the block."
        | oldBreakBlock |
        oldBreakBlock := breakBlock.
        ^[breakBlock := [:arg | ^arg].
            aBlock value]
                ensure: [breakBlock := oldBreakBlock]
    ]
]


이 클래스는 블록 내에서 ^를 사용할 때처럼 전체 메서드를 종료할 필요 없이 블록의 실행을 중단시키는 방법을 제공한다. #ensure: 을 사용하면 breakBlock 이 호출되거나 오류가 언와인에 의해 처리된다 하더라도 오래된 'break block'이 복구되도록 보장한다 (그래서 'ensure(보장하다)'로 명명되었다).


breakBlock의 정의는 극히 단순하다; 이는 블록의 언와인딩 기능을 보여주는 예로, 이미 사용해보았을 것이다.

(history includesKey: num)
    ifTrue: [ ^self error: 'Duplicate check number' ].


미처 생각지도 못하고 #ensure:를 사용해 온 사람도 있을 것이다. 예를 들어 File>>#withReadStreamDo: 는 이를 이용해 블록을 떠나면 파일이 닫히도록 확보한다.


처리기 스택 언와인딩 시 주의사항

스몰토크와 다른 언어 간 한 가지 중요한 차이는 처리기를 호출 시 스택이 언와인드되지 않는다는 사실이다. 스몰토크 예외 시스템은 이러한 방식으로 설계된 이유는 이러한 차이점 때문에 고장나는 코드를 작성하는 경우는 드물고, 스택이 언와인드되어 있다면 #resume: 기능은 의미가 없기 때문이다. 스택은 추후 언와인드해도 충분하며, 너무 일찍 하면 다시 와인딩하기가 그리 쉽지 않다.


거의 대부분의 애플리케이션에서와 마찬가지로 이는 중요하지 않을 것이지만 기술적으로 구문을 크게 변경시키기 때문에 유념해야 한다. 이것이 중요한 한 가지 사례는 #ensure: 블록 그리고 예외 처리기를 이용할 때이다. 비교를 위해 아래 스몰토크 코드는:

| n |
n := 42.
[[self error: 'error'] ensure: [n := 24]]
    on: Error
    do: [:sig | n printNl. sig return].
n printNl.


스크립트상에 '42' 다음에 '24'를 입력할 것인데, sig return 이 호출되어 스택을 언와인드하기 전까지는 n := 24 가 실행되지 않을 것이기 때문이다. 이와 유사한 Java 코드는 다르게 행동한다:

int n = 42;
try
    {
        try {throw new Exception ("42");}
        finally {n = 24;}
    }

catch (Exception e)
    {
        System.out.println (n);
    }
System.out.println (n);


이는 '24'를 두 번 인쇄하는데, catch 블록을 실행하기 전에 스택이 언와인드되기 때문이다.


스몰토크 내부에서 찾을 수 있는 훌륭한 기능

다른 여느 것과 마찬가지로 결국 이렇게 자문할 것이다: 어떻게 이렇게 됐을까? 따라서 이번 장은 이에 대한 답을 제시하고자 한다.

배열은 어떻게 작동하는가

스몰토크는 선택해야 할 사전 정의된 클래스를 최적으로 제공한다. 하지만 결국은 새로운 기본 데이터 구조를 코딩해야 할 때가 올 것이다. 스몰토크의 가장 기본적인 저장공간 할당 기능은 배열이기 때문에 이러한 유형의 저장공간에 효율적으로 접근하기 위해서는 배열을 어떻게 사용하는지를 이해하는 것이 중요하다.


Array 클래스. 앞의 예제들을 통해 이미 Array 클래스를 소개한 바 있는데, 그 사용은 꽤 분명하다. 많은 애플리케이션에서 배열은 당신의 요구를 충족시킬 것이다-새 클래스에 배열이 필요하면 인스턴스 변수를 유지하고, 새 Array를 변수로 할당한 후 인스턴스 변수를 통해 배열 접근성을 전송하면 된다.


이 기법은 저장공간을 낭비하는 일이긴 하나 문자열과 같은 객체에도 작용한다. Array 객체는 배열 내 각 슬롯마다 스몰토크 포인터를 사용한다; 정확한 크기는 프로그래머에겐 보이지만 머신의 워드 크기 정도로 짐작할 수 있다.[19] 따라서 문자의 배열을 저장하기 위한 Array가 작용하긴 하지만 충분하지 않다.


저수준에서 Array. 한 단계 낮은 데이터 구조로 내려가보자. ByteArray는 Array와 많이 비슷하지만 각 슬롯은 0부터 255까지 정수만 보유하고, 각 슬롯은 1 바이트의 저장공간만 사용한다. 각 배열 슬롯에 적은 수만 저장해도 된다면 Array보다 이것이 더 효율적인 선택일 것이다. 짐작했겠지만 이것은 String 이 사용하는 문자열 타입이다.


아하! 6.9절로 돌아가 계층구조를 다시 살펴보면 String이 ByteArray로부터 상속되지 않음을 눈치 챌 것이다. 이유를 알아보려면 다시 한 수준 아래를 뒤져 클래스의 인스턴스 구조를 마련하기 위한 기본 메서드에 도달해야 한다.


NiledArray 예제를 구현할 때 <shape: #inherit>을 사용하였다. 그 모양은 주어진 클래스 내에서 생성된 스몰토크 객체의 기본 구조와 정확히 같다. 그 차이점을 아래 소개하고자 한다.


Nothing 기본 모양은 가장 단순한 스몰토크 객체를 명시한다. 객체는 인스턴스 변수를 보관하는 데 필요한 저장공간으로만 구성된다. C에서는 0 또는 그 이상의 스칼라 필드가 있는 간단한 구조일 것이다.[20]


#pointer 모든 인스턴스 변수에 대한 저장공간이 아직 할당되어 있지만 클래스의 객체는 new: 메시지로 생성되어야 한다. 인자로서 new: 에 전달된 숫자는 인스턴스 변수를 위한 공간뿐 아니라 새로운 객체로 하여금 명명되지 않은 (색인된) 저장공간이 할당된 다수의 슬롯을 갖도록 만들기도 한다. C에서 비교하자면 몇몇 스칼라 필드가 있는 구조에 더해 끝에는 포인터의 배열이 따라오는, 동적으로 할당된 구조가 해당할 것이다.


#byte new:가 명시한 대로 할당된 저장공간은 바이트의 배열이다. C에서 비교하자면 스칼라 필드[21] 다음에 char의 배열에 따라오는, 동적으로 할당된 구조가 해당할 것이다.


#word new:가 명시한 대로 할당된 저장공간은 C unsigned long의 배열로, 스몰토크에서는 Integer 객체로 표현되는 것들이다. C에서 비교하자면 스칼라 필드 다음에 long의 배열이 따라오는, 동적으로 할당된 구조가 해당할 것이다. 이러한 서브클래스 유형은 스몰토크에서 몇몇 장소에서만 사용된다.


#character new:가 명시한 대로 할당된 저장공간은 문자의 배열이다. C에서 비교하자면 스칼라 필드 다음에 char의 배열이 따라오는, 동적으로 할당된 구조가 해당할 것이다.


좀 더 특수화된 사용을 목적으로 한 모양도 많다. 이는 모두 다른 데이터 타입이지만 동일한 유형의 '배열과 같은' 행위를 명시한다.


이러한 새 배열로 어떻게 접근할까? 인스턴스 변수로 접근하는 방법은 이미 알고 있을 것이다-이름을 통해. 하지만 이 새로운 저장공간을 위한 이름은 없는 듯 보인다. 객체가 이 저장공간으로 접근하기 위해서는 at:, at:put: 등과 같은 배열 타입의 메시지를 자신에게 전송한다.


객체가 이러한 메시지로 새로운 수준의 해석을 추가하길 원할 때 바로 문제가 된다. Dictionary를 생각해보자-이는 포인터를 보유하는 객체지만 그것의 at: 메시지는 그 저장공간의 정수 색인이 아니라 키와 관련된 것이다. at: 메시지를 재정의한 시점에서는 어떻게 기본 저장공간으로 접근할까?


그에 대한 답은, at:과 at:put: 메시지가 다른 추상화를 제공하도록 정의되었을 때조차 스몰토크는 기본 저장공간으로 접근하게 될 basicAt:과 basicAt:put: 을 정의했다는 것이다.


추상화에서 약간 혼동될 수도 있는데, 사실상 꽤 단순함을 예를 통해 보이고자 한다. 스몰토크 배열은 1부터 시작하는 경향이 있다; 허용 가능한 범위가 임의적인 배열 타입을 정의해보자.

ArrayedCollection subclass: RangedArray [
    | offset |
    <comment: 'I am an Array whose base is arbitrary'>
    RangedArray class >> new: size [
        <category: 'instance creation'>
        ^self new: size base: 1
    ]
    RangedArray class >> new: size base: b [
        <category: 'instance creation'>
        ^(super new: size) init: b
    ]
    
    init: b [
        <category: 'init'>
        offset := (b - 1). "- 1 because basicAt: works with a 1 base"
        ^self
    ]
    
    rangeCheck: i [
        <category: 'basic'>
        (i <= offset) | (i > (offset + self basicSize)) ifTrue: [
            'Bad index value: ' printOn: stderr.
            i printOn: stderr.
            Character nl printOn: stderr.
            ^self error: 'illegal index'
        ]
    ]
    at: [
        self rangeCheck: i.
        ^self basicAt: i - offset
    ]
    at: i put: v [
        self rangeCheck: i.
        ^self basicAt: i- offset put: v
    ]
]


코드에는 두 부분이 있다; 배열이 시작하길 원하는 색인을 단순히 기록하는 initialization, 그리고 요청된 색인을 조정하여 기본 저장공간이 대신 1부터 시작되는 색인을 수신하도록 하는 at: 메시지. 범위 검사를 포함시켰다; 그 사용을 곧바로 소개하겠다.

a := RangedArray new: 10 base: 5.
a at: 5 put: 0
a at: 4 put: 1


4는 우리 기준(base)인 5보다 아래기 때문에 범위 검사의 오류가 발생한다. 하지만 이 검사는 우리가 잘못한 행위 이상을 잡아낼 수 있다!

a do: [:x| x printNl]


do: 메시지 처리가 잘못되었다! 스택 추적은 많은 정보를 알려준다:

RangedArray>>#rangeCheck:
RangedArray>>#at:
RangedArray>>#do:


우리 코드가 do: 메시지를 수신하였다. 우리가 정의하진 않았으므로 기존 do: 처리를 상속하였다. Integer 루프가 구성되고, 코드 블록이 호출되었으며, 우리 고유의 at: 코드가 호출되었음을 볼 수 있다. 범위를 확인하자 유효하지 않은(illegal) 색인을 가두었다. 우연의 일치로 이 버전의 범위 검사 코드는 색인을 버리기까지 한다. do:에서는 모든 배열이 1부터 시작한다고 가정했음을 알 수 있다.


즉각적인 수정은 분명하다; 우리만의 do:를 구현하는 것이다.

RangedArray extend [
    do: aBlock [
        <category: 'basic'>
        1 to: (self basicSize) do: [:x|
            aBlock value: (self basicAt: x)
        ]
    ]
]


하지만 더 깊은 문제가 발생한다. 부모 클래스가 만약 색인이 1부터 시작된다는 사실을 가정할 정도로 충분히 알고 있다고 생각한다면[22], 왜 스스로 basicAt: 을 호출할 수 있다고는 생각하지 못할까? 두 가지 선택 중 부모 클래스의 설계자가 문제를 적게 야기할 법한 것을 선택했기 때문이다; 사실 모든 표준 스몰토크 컬렉션은 색인이 1부터 시작되지만 모두 그런 것은 아니므로 basicAt: 이 작동할 것이다.[23]


객체 지향 방법론은 하나의 객체가 다른 객체를 완전히 이해할 수 없어야 한다고 말한다. 하지만 더 높은 클래스와 그 서브클래스 간에는 어떤 privacy가 존재해야 하는가? 서브클래스는 그 슈퍼클래스와 관련해 얼마나 추측을 하고, 슈퍼클래스는 서브클래스의 독립성(sovereignty)을 침해하기까지 얼마나 많은 추측을 내릴 수 있을까?


이에 대해 답하기 쉬운 답은 없는데 예제만 하나 소개하겠다. 이런 특별한 문제의 경우 한 가지 쉬운 해결책이 있다. 최상의 효율성을 목적으로 저장공간에 접근할 필요가 없는 경우 기존 배열 클래스를 사용할 수 있다. 모든 접근이 중요할 경우에는 저장공간을 자신의 객체의 중심부분으로 하면 가장 빠른 접근을 허용한다-하지만 이 영역으로 이동하면 상속과 다형성이 까다로워지는데, 각 수준이 기반이 되는 배열의 사용을 다른 수준과 조정해야 하기 때문이다.


두 가지 종류의 상등관계(equality)

처음에는 제 2장에서 살펴보았듯 스몰토크는 dictionary를 다른 것과 붙여서 가령 #word 와 같이 사용하는 반면 우리는 일반적으로 'word'를 사용한다. 전자는 Symbol 클래스로부터 파생되고, 후자는 String 클래스에서 파생된 것으로 드러난다. Symbol과 String의 실제적 차이는 무엇일까? 이에 답하기 위해 C와 비교를 하고자 한다.


C에서는 문자열을 비교하기 위한 함수가 있다면 아래와 같이 작성할 것이다:

streq(char *p, char *q)
{
	return (p == q);
}


그렇지만 이건 분명히 잘못되었다! 이유는 문자열의 복사본이 두 개가 있는데, 서로 내용은 동일하지만 고유의 어드레스에 있기 때문이다. 올바른 문자열 비교는 문자열을 하나씩 훑어보고 각 요소를 비교해야 한다.


스몰토크에서도 정확히 동일한 문제가 존재하긴 하지만 저장공간 어드레스를 조작하는 것과 관련된 세부내용은 숨겨져 있다. 두 개의 스몰토크 문자열을 갖고 있는데 동일한 내용이라면 같은 저장공간 어드레스에 있는지 굳이 알 필요가 없다. 스몰토크 용어로 말하자면, 우리는 그들이 동일한 객체인지 알지 못한다.


스몰토크 dictionary는 자주 검색된다. 검색의 속도를 증가시키 위해서는 각 요소의 문자를 비교하지 않고 어드레스 자체만 비교하는 편이 좋을 것이다. 이를 위해선 내용이 동일한 모든 문자열이 동일한 객체가 되도록 확보할 필요가 있다. 아래와 같이 생성된 String 클래스는:

y := 'Hello'


이러한 조건을 충족시키지 못한다. 이 행을 실행할 때마다 새 객체도 얻게 될 것이다. 하지만 이와 매우 비슷한 클래스에 해당하는 Symbol은 항상 동일한 객체를 리턴할 것이다:

y := #Hello


대체로 문자열은 거의 모든 작업에 사용 가능하다. 문자열을 검색하는 성능 결정적인 함수로 들어간다면 Symbol로 전환할 수 있다. Symbol을 생성하는 데 더 많은 수고가 들며, Symbol에 대한 메모리는 절대 해제되지 않는다 (계속 동일한 객체를 리턴하도록 확보하기 위해서는 클래스가 무기한으로 계산해야 하기 때문이다). 이를 사용할 수는 있으나 주의해야 한다.


본 지침서는 strcmp()와 비슷한 유형의 상등관계 검사를 사용하였다. '이것이 동일한 객체인가'라고 질문해야 한다면 = 대신 == 연산자를 사용하라:

x := y := 'Hello'
(x = y) printNl
(x == y) printNl
y := 'Hel', 'lo'
(x = y) printNl
(x == y) printNl
x := #Hello
y := #Hello
(x = y) printNl
(x == y) printNl


C 용어를 사용하자면, =는 strcmp()과 같이 내용을 비교하고, ==는 포인터 비교와 같이 저장공간 어드레스를 비교한다.


메타클래스에 관한 진실

머지않아 누구든 Object 클래스에서 #new 메서드의 구현을 검색한다. 놀랍게도 아무도 찾지 못한다; 정말로 똑똑하다면 이미지에서 #new의 구현자를 검색하고, Behavior에 의해 구현되었음을 발견할 것인데, 알고 보니 Behavior는 Object의 서브클래스가 아닌가! 모두들 입으론 외치지만 소수의 사람만이 이해하는 한 문장에 대한 진실에 눈 뜨기 시작할 것이다: '클래스는 객체이다'.


뭐라고? 클래스가 객체라니?!?! 자세히 설명하겠다.


이미지를 열고 mono-spaced 폰트로 인쇄된 텍스트를 입력하라.

st> Set superclass!
HashedCollection

st> HashedCollection superclass!
Collection

st> Collection superclass!
Object

st> Object superclass!
nil


여기까진 새로운 내용이 없다. 약간 다른 걸 시도해보자:

st> #(1 2 3) class!
Array

st> '123' class!
String

st> Set class!
Set class

st> Set class class!
Metaclass


여기서 이상한 Set class 라는 것이 사실 '메타 클래스'라고 불림을 이해하는가...계속 살펴보자:

st> ^Set class superclass!
Collection class

st> ^Collection class superclass!
Object class


클래스와 계층구조 사이에 '병렬식' 계층구조가 보인다. 당신이 클래스를 생성하면 스몰토크는 메타클래스를 생성한다; 그리고 클래스가 그 인스턴스에 대한 메서드가 어떻게 작용하는지 설명하는 것처럼, 메타클래스는 동일한 클래스에 대한 클래스 메서드가 어떻게 작용하는지 설명한다.


Set는 메타클래스의 인스턴스이므로, #new 클래스 메서드를 호출하면 Set class에 의해 구현되는 인스턴스 메서드를 호출하고 있다고 말할 수 있다. 간단하게 말하자면, 클래스 메서드는 거짓말이다: 메타클래스의 인스턴스가 이해하는 인스턴스 메서드에 불과하다.


이제 Object class superclass가 nil class, 즉 UndefinedObject를 응답할 것으로 기대할 것이다. 하지만 #new가 아직 구현되지 않았음을 확인하였으므로...시도해보자:

st> ^Object class superclass!
Class


헛?! 크게 말해보자: Object class 클래스는 Class 클래스로부터 상속한다. Class는 모든 메타클래스의 추상적 슈퍼클래스이고, 이미지에 클래스를 생성하도록 해주는 로직을 제공한다. 하지만 여기서 끝이 아니다:

st> ^Class superclass!
ClassDescription

st> ^ClassDescription superclass!
Behavior

st> ^Behavior superclass!
Object


Class는 다른 클래스들의 서브클래스다. ClassDescription은 추상적이다; Behavior는 구체적이지만, 클래스로 하여금 명명된 인스턴스 변수, 클래스 주석 등을 갖도록 허용하는 상태나 메서드가 부족하다. 그 인스턴스는 light-weight(가벼운) 클래스라 부르는데, 구분된 메타클래스가 없는 대신 모두들 Behavior 스스로를 메타클래스로서 공유하기 때문이다.


Behavior superclass를 평가하면서 우리는 Object 클래스로 다시 올라가보았다: Object는 메타클래스뿐만 아니라 모든 인스턴스의 슈퍼클래스이다. 이렇게 복잡한 시스템은 너무나도 강력하고 매우 흥미로운 일들을 하도록 해주는데, 어쩌면 인식하진 못한 채 이미 시도해보았을 수도 있을 것이다-가령 클래스 메서드에서 #error: 또는 #shouldNotImplement와 같은 메서드의 사용.


이제 마지막 질문과 최종 단계가 남았다: 메타클래스는 무엇의 인스턴스일까? 질문은 이해가 된다: 모든 것이 클래스를 갖고 있다면 메타클래스 역시 그래야하지 않을까?


아래를 계산해보라:

st> meta := Set class
st> 0 to: 4 do: [ :i |
st>     i timesRepeat: [ Transcript space ]
st>     meta printNl
st>     meta := meta class
st> ]
Set class
    Metaclass
        Metaclass class
            Metaclass
                Metaclass class
0


  1. class를 반복하여 전송하면 Metaclass 클래스[24]로 구성된 루프와 Metaclass class라는 그 고유의 메타클래스를 야기하는 듯 보인다. Metaclass 클래스가 자신의 인스턴스의 인스턴스인 것처럼 보인다.


Metaclass의 역할을 이해하려면 클래스 생성이 그 곳에서 구현된다는 사실을 이해하면 유용하겠다. 생각해보라.


  • Random class는 인스턴스의 무작위 숫자 시드(seed)의 생성과 초기화를 구현한다; 이와 비슷하게 Metaclass class는 그것의 인스턴스, 즉 메타클래스의 생성과 초기화를 구현한다.
  • Metaclass는 그것의 인스턴스, 즉 클래스(Class의 서브클래스)의 생성과 초기화를 구현한다.


원(circle)은 닫혔다. 결국 이 메커니즘은 클래스의 자체 정의를 위한 깔끔하고 우아하며 (어느 정도 생각 후) 이해 가능한 기능을 구현한다. 다시 말하자면, 클래스가 서로에 관해 이야기하도록 허용하면서 브라우저의 생성을 위한 기반을 제시한다는 말이다.


스몰토크 성능에 관한 진실

모두들 스몰토크는 느리다고 말하지만 이 말이 전적으로 옳다고는 할 수 없는 이유가 세 가지 있다. 첫째, 그래픽 애플리케이션에서 대부분의 시간은 사용자가 '무언가를 하도록' 기다리는 데 소비되고, 스크립팅 애플리케이션(특히 GNU Smalltalk 전문분야)에서 대부분의 시간은 디스크 I/O에 소비된다; 스몰토크에서 이동하는 판매원의 문제를 구현하는 것은 사실상 느릴 수 있지만, 대부분의 실제 애플리케이션에서는 성능을 스몰토크의 힘과 개발 속도로 대신할 수 있다.


둘째, 스몰토크의 자동 메모리 관리는 C의 수동 메모리 관리보다 빠르다. 대부분 C 프로그램은 C 또는 C++에 이용 가능한 쓰레기 수집 시스템 중 하나로 재연결 시 속도가 높아진다.


셋째, 스몰토크 가상 머신 중에 Self 환경(최적화된 C의 속도 절반에 달하는)으로 최적화된 머신은 매우 소수이나 스몰토크 코드에서 일부 최적화를 실행하여 naïve 바이트코드 해석기보다 몇 배 빠르게 실행된다. 무엇보다 Java에서 자주 보았을 것과 같은[25] JIT(just-in-time) 컴파일러를 개발한 Peter Deutsch는 스몰토크와 같은 언어를 구현하려면 구현자는 속임수를 쓸 수밖에 없음을 발견했다...하지만 걸리지만 않는다면 괜찮다. 다시 말하자면, 언어 구문을 해치지만 않는다면 괜찮다는 의미다. 이러한 최적화의 예를 몇 가지 살펴보자.


자주 사용되는 특정 '특수 선택자(special selector)'의 경우 컴파일러는 send-message 바이트코드 대신 send-special-selector 바이트코드를 보낸다. 특수 선택자는 세 가지 행위 중 하나를 갖는다:


  • 몇몇 선택자는 공간을 절약하기 위해 특수 바이트코드로만 할당된다. 예로 #do: 를 들 수 있다.
  • 세 가지 선택자는 (#at:, #at:put:, #size) 특별한 캐싱 최적화의 대상이기 때문에 특수 바이트코드로 할당된다. 이러한 선택자는 종종 가상 머신 프리미티브를 호출하는 결과를 낳아, GNU Smalltalk는 선택자들을 전송한 결과 어떤 프리미티브가 마지막으로 호출되었는지 기억한다. 동일한 클래스에 대해 #at: 를 100회 전송하면 마지막 99회 전송은 직접 primitive로 매핑되어 메서드 검색 단계를 건너뛴다.
  • 일부 수신자 클래스와 특수 선택자 쌍의 경우 해석기가 클래스에서 메서드를 절대 검색하지 않는다; 대신 특정 primitive로 묶인 동일한 코드를 신속하게 실행한다. 물론 일반적으로 특수 선택자의 수신자 또는 인자가 no-lookup 쌍을 만드는 올바른 클래스의 것이 아닌 특수 선택자가 일반적으로 검색된다.


No-lookup 메서드는 primitive 숫자 명세(number specification)인 <primitive: xx>를 포함하긴 하지만 #perform: ... 메시지 전송을 통해 메서드로 접근했을 때에만 사용된다. 메서드는 보통 검색되지 않기 때문에 보통 primitive 이름 명세를 제거한다고 해서 primitive의 실행을 막을 수는 없다. No-lookup 쌍을 아래 열거하겠다:

Integer/Integer
Float/Integer
Float/Float
for + - * = ~= > < >= <=
Integer/Integer for // \\\\ bitOr: bitShift: bitAnd:
여느 객체의 쌍 for == isNil notNil class
BlockClosure for value value: blockCopy:[26]


그 외 메시지는 컴파일러에 의해 오픈코딩(open coded)된다. 즉, 이러한 메시지에 대한 메시지 전송은 없다는 말이다-컴파일러가 임시 변수 없이, 그리고 올바른 장소에 올바른 인자를 가진 블록을 보면 컴파일러는 다음과 같이 jump bytecodes를 이용해 그것들을 언와인드한다:

to:by:do: if the second argument is an integer literal
to:do:
timesRepeat:
and:, or:
ifTrue:ifFalse:, ifFalse:ifTrue:, ifTrue:, ifFalse:
whileTrue:, whileFalse:


다른 작은 최적화도 이루어진다. 일부는 컴파일된 바이트코드에서 실행되는 핍홀 최적화기(peephole optimizer)에 의해 실행된다. 아니면 가령 GNU Smalltalk가 스택에서 부울 값을 밀면 자동으로 다음 바이트코드가 jump(보통은 위의 대부분 오픈코딩된 메시지로부터 야기된 공통 패턴)인지 확인하고, 두 바이트코드의 실행을 결합한다. 이 모든 조각은 이러한 방식으로 최적화가 가능하다:

1 to: 5 do: [ :i | ... ]
a < b and: [ ... ]
myObject isNil ifTrue: [ ... ]


이것이 전부다. 좀 더 알고 싶다면 libgst/interp-bc.inl 에서 가상 머신의 소스 코드와 libgst/comp.c 에서 컴파일을 살펴보라.


끝맺음말

하나의 문서에서 어느 내용까지 다루는지는 언제나 문제가 된다. 이 시점에서 당신은 클래스를 어떻게 생성하는지 알고 있을 것이다. 상속, 다형성, 스몰토크의 기본 저장공간 관리 메커니즘의 사용 방법도 알고 있다. 스몰토크의 강력한 클래스를 샘플링하는 방법도 살펴보았을 것이다. 따라서 나머지 본문에서는 단순히 향후 연구 영역을 지적하고자 한다; 어쩌면 본 문서의 최신 버전에서 다룰지도 모르겠다.


스몰토크 소스 코드 보기

시스템 메서드에 대한 소스 코드를 살펴보면 많은 경험을 얻을 수 있다; 이 소스 코드는 모두 눈으로 확인할 수 있다: 데이터 구조 클래스, 클래스가 스스로를 객체가 되도록 만들고 클래스를 갖게 만드는 마법의 내부, 스몰토크 자체로 작성된 컴파일러, Smalltalk GUI를 구현하는 클래스와 socket을 둘러싸는 클래스.


객체를 수집하는 기타 방법

Array, ByteArray, Dictionary, Set, 그 외 다양한 스트림을 살펴보았다. Bag, OrderedCollection, SortedCollection 클래스를 살펴보길 원할 것이다. 특별한 목적이라면 CObject와 CType 계층구조를 연구하길 원할 수도 있다.


제어의 흐름

GNU Smalltalk는 실행에 대한 비선점(non-preemptive) 다중 쓰레드를 지원한다. 상태는 Process 클래스 객체에 포함된다; Semaphore와 ProcessorScheduler 클래스를 살펴보길 바란다.


스몰토크 가상 머신

GNU Smalltalk는 가상 명령어 집합으로서 구현된다. –d option으로 GNU Smalltalk를 호출함으로써 명령 행에 파일로 생성된 바이트 옵코드(byte opcode)가 로딩됨을 확인할 수 있다. 이와 유사하게 –e로 GNU Smalltalk를 실행하면 자신의 메서드에서 명령어의 실행을 추적할 것이다.

GNU Smalltalk 소스를 살펴보면 명령어 집합에 대한 더 많은 정보를 얻을 수 있다. Smalltalk의 본래 디자이저 두 명이 작성한 고전 서적에 설명된 집합을 기반으로 하면서 약간의 수정을 추가한 것이다: Smalltalk-80: The Language and its Implementation (저자: Adele Goldberg와 David Robson).


도움 얻기

Usenet comp.lang.smalltalk 뉴스그룹은 스몰토크의 경험이 많은 사람들이 읽고 있다. 여러 상업적 스몰토크 구현들도 있다; 이러한 지원을 구매할 수도 있지만 비용이 저렴하진 않다. 그 중에 GNU Smalltalk 시스템은 아래로 메일링 리스트를 시도해볼 수 있다:

help-smalltalk@gnu.org

보장할 순 없지만 구독자들이 최선을 다해 답해줄 것이다!


스몰토크 구문의 간단한 개요

스몰토크의 힘은 그것의 객체 처리로부터 발생한다. 본 문서에서는 필요 시 괄호로 된 표현식을 엄격히 사용함으로써 구문의 문제를 피하고 있다. 괄호가 너무 많아 읽기가 힘든 코드를 야기하면 스몰토크의 구문에 대한 지식이 표현식을 단순화시켜줄 수 있다. 대체로 표현식이 어떻게 파싱할 것인지 자신도 말하기가 힘들다면 그 다음 사람에게도 힘들 것이다.


아래 표현은 문법에서 관련된 요소 몇 가지를 따로따로 나타낸다. 우리는 EBNF 문법 스타일을 사용하는데, 그 형태는 다음과 같다:

[ ...   ]

이는 '...'이 0회 또는 1회 발생 가능하다는 말이다.


[ ...   ]*

위는 0 또는 그 이상의 수이고;

[ ...   ]+


위는 0 또는 그 이상의 수이며;

...   | ...   [  |  ...   ]*


위는 변형체 중 하나를 선택해야 함을 의미한다. 큰 따옴표 내 문자는 리터럴 문자를 나타낸다. 대부분 요소는 여백으로 구분된다; 이것이 허용되지 않는 곳에서는 요소들이 여백 없이 표현된다.


methods: ''!'' id [''class''] ''methodsFor:'' string ''!'' [method ''!'']
''!''


메서드는 먼저 클래스를 명명하고 (id 요소), 인스턴스 메서드 대신 클래스 메서드를 추가 시 'class'를 명시하며, methodsFor: 메시지로 문자열 인자를 전송함으로써 메서드가 소개된다. 각 메서드는 '!'로 종료된다; 한 행에 두 개의 감탄부호(bang)는 (중간에 공백이 있는) 새 메서드의 끝을 나타낸다.

method: message [pragma] [temps] exprs
message: id | binsel id | [keysel id]+
pragma: ''<'' keymsg ''>''
temps: ''|'' [id]* ''|''


메서드 정의는 일종의 템플릿으로 시작한다. 처리될 메시지는 인자 대신 식별자와 철자가 적힌 메시지명으로 명시된다. 특별한 유형의 정의는 pragma이다; 본 지침서에서 다루지 않은 내용인데, 본래 기반이 되는 스몰토크 가상 머신으로 인터페이스뿐만 아니라 특히 메서드를 표시하는 방법을 제공한다. temps는 지역 변수의 선언이다. 마지막은 (곧 다룰) 메서드의 구현을 위한 실제 코드이다.

unit: id | literal | block | arrayconstructor | ''('' expr '')''
unaryexpr: unit [ id ]+
primary: unit | unaryexpr


스몰토크 표현식의 'unit(단위)'다. unit은 하나의 스몰토크 값을 나타내는데 구문적 우선순위가 가장 높다. unaryexpr는 다수의 단항 메시지를 수신하는 단위일 뿐이다. unaryexpr의 우선순위가 그 다음으로 높다. primary는 위의 것들 중 하나를 대상으로 한 편리한 좌측 이름(left-hand-side name)을 나타낼 뿐이다.

exprs: [expr ''.'']* [[''^''] expr]
expr: [id '':='']* expr2
expr2: primary | msgexpr [ '';'' cascade ]*


표현식 순서는 점으로 구분되어 있고 리턴된 값 (^)로 끝날 수 있다. 선두에 할당(leading assignment)이 존재할 수 있다; C와 달리 할당은 단순한 변수명에만 적용된다. 표현식은 primary이거나 (우선순위가 가장 높은) 좀 더 복잡한 메시지일 수 있다. Cascade는 primary 구성에 적용되지 않는데, 구성을 요구하기엔 너무 단순하기 때문이다. 모든 primary는 단항으로 구성되므로 단항 메시지만 더 추가할 수 있다:

1234 printNl printNl printNl
msgexpr: unaryexpr | binexpr | keyexpr


복잡한 메시지는 단항 메시지 (이미 다룬 내용), 이항 메시지 (+, - 등) 또는 키워드 메시지 (at:, new:, ...) 중 하나다. 단항 메시지의 우선순위가 가장 높고, 이항 메시지, 키워드 메시지가 따라온다. 아래 메시지의 두 가지 버전을 살펴보라. 두 번째는 기본 우선순위를 표시하기 위해 괄호를 추가시켰다.

myvar at: 2 + 3 put: 4
mybool ifTrue: [ ^ 2 / 4 roundup ]
(myvar at: (2 + 3) put: (4))
(mybool ifTrue: ([ ^ (2 / (4 roundup)) ]))
cascade: id | binmsg | keymsg


cascade는 마지막으로 사용된 것과 동일한 객체로 추가 메시지를 명령할 때 사용된다. 따라서 세가지 유형의 메시지를 (id 는 단항 메시지를 전송하는 방식) 전송할 수 있다.

binexpr: primary binmsg [ binmsg ]*
binmsg: binsel primary
binsel: binchar[binchar]


primary가 식별한 객체로 이항 메시지가 전송된다. 각 이항 메시지는 하나 또는 두 개의 문자로부터 구성된 이항 선택자이고, primary에 의해 역시 제공된 인자이다.

1 + 2 - 3 / 4


위는 아래와 같이 파싱한다:

(((1 + 2) - 3) / 4)
keyexpr: keyexpr2 keymsg
keyexpr2: binexpr | primary
keymsg: [keysel keyw2]+
keysel: id'':''


키워드 표현식은 선택자가 콜론이 붙은 식별자로 구성되었다는 점을 제외하면 이항 표현식과 많이 닮았다. 이항 함수로의 인자는 primary로부터만 가능하며, 키워드로의 인자는 이항 표현식이나 primary 표현식으로부터 가능하다. 그 이유는 키워드의 우선순위가 가장 낮기 때문이다.

block: ''['' [['':'' id]* ''|'' ] [temps] exprs '']''


코드 블록은 스몰토크 표현식의 집합체 주위를 둘러싼 사각 괄호이다. 앞의 ': id' 부분은 블록 인자를 위한 것이다. 블록이 자신의 임시 변수를 가질 수 있음을 주목하라.

arrayconstructor: ''{'' exprs ''}''


본 지침서에서 다루지 않는 이 구문은 값이 리터럴이 아닌 배열을 생성하지만 대신 런타임 시 계산되는 배열을 생성하도록 허용한다. 두 개의 기호, #a와 #b의 Array를 야기하는 #(a b)와, 변수 a의 내용과 c와 b를 합한 결과를 요소로 하는 Array를 야기하는 {a. b+c}를 비교하라.

literal: number | string | charconst | symconst | arrayconst | binding |
eval
arrayconst: ''#'' array | ''#'' bytearray
bytearray: ''['' [number]* '']''
array: ''('' [literal | array | bytearray | arraysym | ]* '')''
number: [[dig]+ ''r''] [''-''] [alphanum]+ [''.'' [alphanum]+] [exp
[''-''][dig]+].
string: "'"[char]*"'"
charconst: ''$''char
symconst: ''#''symbol | ''#''string
arraysym: [id | '':'']*
exp: ''d'' | ''e'' | ''q'' | ''s''


이 상수 중 다수의 사용은 이미 소개한 바 있다. 본 지침서에서 다루진 않았지만 숫자는 앞에 기준(base)을 명시하고 뒤에 과학적 표기법(trailing scientific notation)을 가질 수 있다. 문자, 문자열, 기호 상수의 예제도 살펴보았다. 배열 상수는 충분히 단순하며 그 모양은 다음과 같다:

a := #(1 2 'Hi' $x #Hello 4 16r3F)


요소가 0부터 255까지 정수로 제한되는 ByteArray 상수도 있는데, 그 모양은 다음과 같다:

a := #[1 2 34 16r8F 26r3H 253]


마지막으로 부동소수점 상수의 유형에는 정밀도 수준에 따라 세 가지가 있고 (정밀도 높은 순으로 e, d, q), 정확한 계산을 실행하지만 주어진 소수점 수로 비교 연산자를 줄이는 특수 클래스에 대한 scaled-decimal 상수가 있다. 1.23s4 는 '네 자리 유효 십진 숫자가 있는 값 1.23'을 의미한다.

binding: ''#{'' [id ''.'']* id ''}''


이 구문은 지침서에선 사용되지 않았으나, 중괄호 사이에 명명된 클래스로 묶인 Association 리터럴을 야기한다. 예를 들어, #{Class} value는 Class와 같다. 도트 표기법(dot syntax)은 네임스페이스의 지원에 필요하다: #{Smalltalk.Class}는 Smalltalk associationAt: #Class 와 동일하지만 런타임 시보다는 컴파일 시 해결된다.

symbol: id | binsel | keysel[keysel]*


기호는 대부분 메서드명을 표현하는 데 사용된다. 따라서 간단한 식별자, 이항 선택자, 키워드 선택자를 보유할 수 있다:

#hello
#+
#at:put:
eval: ''#'' ''#'' ''('' [temps] exprs '')''


이 구문은 지침서에선 사용되지 않았으며, 컴파일 시 임의의 복합 표현식을 평가하고 결과를 대체하는 결과를 낳는다: 예를 들어, ##(Object allInstances size)는 메서드가 컴파일될 때 이미지에 보유된 Object의 인스턴스 수이다.

id: letter[letter|dig]*
binchar: ''+'' | ''-'' | ''*'' | ''/'' | ''~'' | ''|'' | '','' |
''<'' | ''>'' | ''='' | ''&'' | ''´' | ''?'' | ''\'' | ''%''
alphanum: ''0''..''9'' | ''A''..''Z''
dig: ''0''..''9'

이는 문자의 범주로, 가장 기본 수준에서 어떻게 결합되는지를 표시한 것이다. binchar는 단순히 이항 메시지를 명명 시 결합 가능한 문자를 열거한다.


Notes

  1. 시스템이 기반이 되는 스몰토크 엔진의 성능에 관한 정보를 제공하는 수많은 통계도 인쇄하도록 만들 수도 있다. 이는 스몰토크를 다음으로 시작 시 가능하다:
    localhost$ gst -v
  2. 어떤 테이블일까? 이는 객체의 타입에 따라 결정된다. 객체는 그것이 속한 클래스로서 알려진 타입을 가져야 한다. 각 클래스는 메서드의 테이블을 갖는다. 우리가 생성한 객체의 경우, 객체는 String 클래스의 멤버로서 알려진다. 따라서 String 클래스와 관련된 테이블로 들어가야 한다.
  3. 사실 메시지 printNl은 Object에서 상속되었다. 이는 또 Object에서 상속된 print 메시지를 전송한 후 객체로 printOn: 을 전송하면서, Transcript 객체로 인쇄하도록 명시한다. 이후 String 클래스는 문자를 표준 출력으로 인쇄한다.
  4. GNU Smalltalk는 Bash 또는 GDB와 동일한 방식으로 완성(completion)를 지원한다. 다음 행을 입력하기 위해서는 가령 'x := Arr<TAB> new: 20'를 입력할 수 있다. 이는 긴 이름을 입력할 때 매우 유용한데, IdentityDictionary가 'Ide<TAB>D<TAB>'이 되는 것을 예로 들 수 있겠다. 대문자로 시작하거나 콜론으로 끝나는 모든 것은 완성 가능하다.
  5. 집중해서 읽었다면 여기서 예외의 예제들을 앞장에서 읽었던 기억이 날 것이다.
  6. C와는 달리 스몰토크는 0과 nil 을 구별한다. nil은 아무 것도 아닌(nothing) 객체이고, 가령 이 객체 상에서 수학 연산을 실행한다거나 할 경우 오류를 수신할 것이다. 향후 인스턴스 변수에서 수학 연산을 실행하길 원한다면 숫자 0으로 초기화하는 것이 매우 중요하다.
  7. 디자이너들이 리턴 값을 왜 nil로 디폴트하지 않았을까? 어쩌면 void 함수의 값을 인정하지 않았기 때문인지도 모른다. 결국 스몰토크가 설계된 시절에는 C에 void 데이터 타입은 없었으니까 말이다.
  8. self는 super와 같은데, self의 경우 객체에 대한 타입 계층구조의 하단에서 메서드 검색을 시작하는 반면 super는 현재 수준에서 상향으로 검색을 시작한다는 점이 다르다. 따라서 super를 사용하면 상속을 강제로 이용하지만 self는 메시지의 첫 번째 정의를 찾을 것이다.
  9. 물론 실제 회계 시스템에서는 그러한 정보를 폐기할 리가 절대 없다ㅡ아마도 Dictionary 객체로 넣어, 마무리 짓는 연도(year)로 색인될 것이다. 야심 있는 사람이라면 이러한 개선을 직접 실행해볼지도 모른다.
  10. 조건부를 실행하는 방식으로 인해 조건부 구성체는 스몰토크 언어에 속하지 않고 그 대신 객체의 Boolean 객체에 정의된 행위에 불과하다는 사실이 흥미롭다.
  11. 하나의 키 아래에 있는 두 가지 정보를 연관시키고 싶다면 어떻게 해야할 것인지 궁금증이 발생하는 사람이 있을 것이다. 가령, 가치(value)와 수표 수령인을 연관시킨다고 치자. 이를 위한 방법에는 여러 가지가 있다; 최상의 방법은 해당 정보를 포함하는 새로운 커스텀 객체를 생성하고, 이 객체를 dictionary 내 수표 발행 번호 키에 보관하는 방법일 것이다. dictionary를 값으로서 보관한 후 각 슬롯마다 원하는 만큼의 정보 조각을 보관하는 방법도 (지나친 방법일 수도 있으나) 유효하다!
  12. do: 메시지는 대부분의 스몰토크 콜렉션 타입이 이해한다. Dictionary 클래스뿐만 아니라 집합, 배열, 문자열, 인터벌, 연결된 리스트, 백, 스트림 모두 해당된다. 가령 keysDo: 메시지는 dictionaries로만 작용한다.
  13. 원하는 수만큼 인자를 보유한 배열을 수락하는 valueWithArguments: 메시지도 있다.
  14. Blox GUI를 사용하면 사실 인스펙터 창(Inspector window)이라 불리는 창이 뜬다.
  15. 이 목록은 GNU Smalltalk 저자 Steve Byrne이 제공한 printHierarchy에 대한 예의다.
  16. 스몰토크는 or: 메시지도 제공하는데, 이는 |와는 미묘하게 다르다. or: 는 코드 블록을 취하고, 표현식의 값을 결정하는 것이 불가피할 경우 코드 블록을 호출하기만 한다. 이는 필요할 때에만 ||가 좌측에서 우측으로 평가하는 보장된 C 구문과 유사하다. ((index < 1) or: [index > (self basicSize)]) 와 같은 표현식을 작성하였을 수도 있다. or: 의 두 측면(sides)이 대부분 false일 것으로 예상되므로 이번 경우 한 측면의 평가를 지연시킬 이유가 없다.
  17. GNU Smalltalk 확장은 $<43>에서와 같이 ASCII 코드에 의한 타입 문자도 입력하게 해준다.
  18. Transcript 가 동일한 이름으로 된 창으로 연결된 Blox 에서 실행해보라!
  19. GNU Smalltalk의 경우 C long 의 크기를 사용하며, 주로 32비트이다.
  20. C는 1 또는 그 이상의 수를 필요로 한다; 0은 스몰토크에서 허용된다.
  21. variableByteSubclasses와 variableWordSubclasses에서 인스턴스 변수를 허용하지 않는 다른 스몰토크 구현에선 항상 사실이 아니다.
  22. 사실 GNU Smalltalk에서는 이러한 추측을 하는 메시지가 do: 뿐만은 아니다.
  23. 이 클래스 중 일부는 사실상 성능을 이유로 do: 를 재정의하지만 do: 에 대한 부모 클래스의 구현이 유지되었다 하더라도 작동할 것이다.
  24. ClassDescription의 또 다른 서브클래스로 밝혀짐.
  25. GNU Smalltalk가 실험적 기능으로 포함하는 것과 같다.
  26. 스몰토크 프로그램에서 이 메시지를 전송할 일은 절대 없을 것이다. 이는 컴파일러가 블록을 컴파일 시 사용한다.