SmalltalkObjectsandDesign:Chapter 06
- 제 6 장 컨테이너와 기타 필수적 발상
컨테이너와 기타 필수적 발상
일상 세계에서 컨테이너-다른 것들을 포함하는 것들-는 어디든 존재한다. 항아리, 숟가락, 바구니, 버스, 카운터 톱, 책, CD-ROM, 콩깍지까지 모두 컨테이너에 해당한다. 컨테이너는 심지어 다른 컨테이너를 포함하기도 한다. 내 부엌은 식품 저장실을 포함하고, 식품 저장실은 선반을 포함하며, 선반에는 상자와 캔이 포함되어 있고, 그 안에는 시리얼과 수프가 있다. 내 부엌에는 또 냉장고가 있고, 냉장고는 선반과 문을 포함하는데, 이 둘도 컨테이너에 속한다.
컨테이너는 필수적인 일상의 물체(object)들이며, 우리는 그것들이 필수적 소프트웨어 객체가 될 수 있을 것으로 기대한다. 소프트웨어 객체 사이에서 발생하는 패턴들은 우리가 일상의 물체에서 관찰하는 내용을 반영해야 한다.
프로그래머에게 있어 중요한 컨테이너에는 큐(queue), 스택(stack), 배열(array), 세트(set)가 있다. 객체 지향 프로그래머, 특히 스몰토크 프로그래머는 컨테이너를 collections 라 부르기도 한다. 스몰토크에서 컨테이너 클래스는 모두 Collection 이라는 추상 클래스의 서브클래스에 속하기 때문이다:
이 문서에서는 컬렉션과 컨테이터를 동시에 사용하는것을 목적으로 한다.
위의 계층구조는 IBM Smalltalk 의 container 클래스 몇 개만 표시하였다. 따라서 가장 자주 사용되는 스몰토크 컨테이너와 그들의 구별된 특징을 몇 가지 소개하겠다.
클래스 | 특징 |
Dictionary | 실제 사전처럼 검색 키로 정보를 체계화한다. |
Array | 그 요소들이 연속 슬롯에 배열되어 있다. 크기는 고정되어 있다. |
OrderedCollection | 배열과 마찬가지지만 크기는 증가 또는 감소할 수 있다. |
Set | 그 요소들이 어떤 순서로도 정리되어 있지 않다. 또한 객체는 그 안에서 최대 한 번 발생할 수 있다. |
String | 0개 또는 더 많은 문자를 포함한다. |
ByteArray | 배열과 마찬가지지만 그 요소들은 바이트이다. |
SortedCollection | 정렬된 컬렉션보다 똑똑하고, 일부 정렬 기준에 따라 결정된 정렬대로 요소들을 유지한다. |
IdentityDictionary | 주로 키가 Symbol 또는 SmallInteger 객체일 경우에 적합한 특별하고 효과적인 사전이다. |
Stream (컬렉션과 비슷하지만 컬렉션은 아니다) |
컬렉션보다 똑똑하고, 그것이 마지막으로 접근된 위치를 기억한다. |
컨테이너에 관한 서론에서 마지막으로 해둘 말: 사실적으로 당신이 실행하는 모든 객체 지향 디자인은 다음 장에서 최초로 시도하게 될 크기 조정 가능한 디자인 실습을 포함해 하나 또는 그 이상의 컨테이너를 필요로 할 것이다. 자신의 디자인에서 컨테이너의 필요성을 인지하는 법을 학습하는 것은 훌륭한 객체 지향 디자이너가 되기 위한 중요한 단계이다.
이질성(heterogeneity)과 동질성(homogeneity)
일상 세계에서 서랍과 같은 일부 컨테이너는 서류철 등의 다양한 종류의 사물을 포함하고, 또 어떤 컨테이너는 페이지와 같이 한 가지 종류의 사물만 포함한다. 스몰토크의 컨테이너는 서랍과 같다. 즉, 일반적인 스몰토크 컨테이너들은 그 내용물이 모두 동일한 유형인지 확인하지 않을 것을 의미한다; 정수, whales, 심지어 정수와 whales의 결합도 포함할 수 있다. 서랍과 같이 이질적 요소들을 포함하는 능력을 가진 프로퍼티는 매력적으로 들릴 수는 있으나 잠재적으로 보면 위험하다. 그것이 매력적인 것은 분명하다: 당신이 포함하고자 하는 객체 유형에 작동할 것인지 염려할 필요없이 컨테이너를 골라서 사용하면 된다-어떠한 스몰토크 객체든 포함할 것이다. 반면, 컨테이너가 요소들을 정렬된 순서로 유지할 경우 (다음 절의 실습 참조) 정수와 whales의 결합(mixture)을 포함하거나 whales만 포함하길 원한다면 어떤 장점이 있을까? 아니면 현재 정수를 포함하는 세트가 잇는데 실수로 그 안에 whale을 넣었다고 생각해보라. 모든 요소를 의심하면 whale은 walkback을 통해 2 곱하기(multiplication by two)를 이해할 수 없다고 이의를 제기할 것이다.
연습: 이질성과 동질성
❏ 정렬된 컬렉션은 그 크기가 고정되지 않는 점만 제외하면 배열과 같다. Whale 클래스를 정의한 후 아래를 표시하여 이질성을 살펴보라. (OC 와 같은 전역 변수를 선언할 것을 잊지 말라.)
OC := OrderedCollection new.
OC add: 'a';
Add: 'c'.
"OC add: Whale new."
OC.
인용 부호를 제거하고 다시 시도해보라. 이 연습은 정렬된 컬렉션이 어떤 유형의 객체든 수용할 수 있음을 보여준다. 하지만 OC 에 의미가 있는 무언가를 하고 싶은 경우 그것의 이질적인 내용이 문제가 된다. 아래 코드를 실행하면 walkback이 생성되는 이유가 무얼까?
Pet := 't'.
OC do: [:elem ㅣ Pet := elem , Pet].
Pet
하지만 지금 인용 부호를 대체하여 whale 행에 다시 주석을(re-comment) 달고, 문의 처음부터 끝까지 전체 순서를 표시하면 오류가 사라지고 스몰토크는 'cat'를 표시한다.
❏ Collection 클래스의 또 다른 서브클래스로 SortedCollection 가 있다. SortedCollection 클래스의 인스턴스는 그 객체가 항상 정렬된 순서로 유지되는 컬렉션이다. 아래를 표시하라:
X := SortedCollection new.
X add: 3; add: 2; add: 5.
X
여기서 순서는 누가 정의할까? 모든 SortedCollection 객체는 정렬 연산을 정의하는 정렬 블록을 갖고 있다는 것이 답이 되겠다. 기본 연산은 <= 이다. 표시하기 를 통해 역으로 정렬해보라:
X sortBlock: [:a :b ㅣ a>=b].
X
❏ 정렬된 컬렉션은 어떻게 이질적인가? 당신이 추가하는 객체는 무엇이든 수용하는 세트(set)와 달리 정렬된 컬렉션은 당신이 하나 이상의 객체를 추가하는 순간 비교 메시지를 전송하기 시작한다. 당신이 추가하는 객체가 비교 가능하지 않은 경우, 정렬된 컬렉션은 walkback으로 이의를 제기한다. 예를 들어, 아래를 실행하라:
X add: Whale new.
따라서 당신이 정렬된 컬렉션에 처음으로 추가하는 객체는 어떤 클래스든 될 수 있지만 그 다음에 오는 모든 객체들은 처음 것과 비슷해야(comparable) 할 것이다.
연습: dictionaries
❏ 검색하기-사전, 전화번호부, 소프트웨어 도움말 파일, 관계형 데이터베이스 테이블 등에서-는 인간과 컴퓨터 모두에게 기본적인 활동이다. 이러한 활동에 대한 스몰토크의 컨테이너 클래스는 Dictionary 다. Dictionaries 는 일반적인 스몰토크 애플리케이션에서 가장 널리 사용되는 컨테이너에 속한다. (하지만 Arrays가 더 우세하게 사용된다.) 현재 자신의 이미지에서 이러한 객체들의 상대적 발생을 보고 싶다면 아래를 한 행씩 표시하라:
Set allInstances size.
Dictionary allInstances size.
Array allInstances size.
String allInstances size.
❏ 스몰토크 dictionary 는 스몰토크가 연관(associations)이라 부르는 엔트리로 구성된다. 각 연관은 하나의 키와 하나의 값을 가진다. 아래를 실행하면:
X := Dictionary new.
X at: 'Kilauea' put: 'Most active volcano';
at: 'Denali' put: 'Formerly Mt. McKinley'.
지명을 키로 하고 연관된 장소의 설명을 값으로 둔 두 개의 연관으로 구성된 새로운 사전을 구성한다. 이러한 연관은 아래를 표시하여 검색하거나:
X at: 'Denail'
아래의 구문을 실행해서 전체 사전을 살펴볼 수 있다:
X inspect.
예행 연습: 정체성 대 동등성
여느 객체 시스템과 마찬가지로 스몰토크는 정체성과 동등성 사이에 중요한 차이를 유지한다. 다음 절에서 정체성 dictionaries를 이해하기 위해서는 이러한 차이를 먼저 파악할 필요가 있다.
"두 개의" 객체가 동일(identical)하다고 말하기 위해서는 그들이 실제로 똑같은 객체라고 말할 수 있어야 한다. 이러한 조건을 시험하는 메시지 선택자가 바로 == 이다. 예를 들어, X == X는 X 변수가 어떤 객체를 참조하는지 상관없이 true 객체를 야기한다.
❏ == 메시지는 스몰토크에서 일부 근본적인 질문들을 해결하는 데에 사용할 수도 있다. 먼저, 정수의 각 발생 횟수(occurrence)가 서로 다른 객체를 참조할까, 동일한 객체를 참조할까? 이 질문에 답하려면 아래 두 행을 먼저 하나씩 실행한 후 마지막 행을 표시하라:
X := 3.
Y := 3.
X == Y.
결과는 true 객체기 때문에 우리는 모든 스몰토크에서 3 객체는 하나만 존재한다는 결론을 내린다. (어떤 "작은" 정수도 마찬가지로, 엄밀히 말해 SmallInteger 클래스의 어떤 인스턴스든 여기에 해당한다. 스몰토크의 각 dialect 에는 범위가 있는데, 정수가 SmallInteger 의 인스턴스가 아니라 LargeInteger 의 인스턴스일 때까지를 범위로 한다. IBM Smalltalk 는 -230 에서 230 까지, 또는 -1073741824 부터 +1073741823 까지를 범위로 가진다.)
❏ 정수와 같은 부호: 기껏해야 주어진 스펠링으로 된 하나의 부호가 가능하며, 아래를 한 번에 한 줄씩 실행하고 마지막 줄은 표시하여 확인할 수 있다:
X := #Hobbes.
Y := #Hobbes.
X == Y.
한편, 이 실험을 아래를 이용해 한 줄씩[1] 반복해보라:
X := 'Hobbes'.
Y := 'Hobbes'.
X == Y.
결과값인 false 는 'Hobbes' 문자열의 두 인스턴스가 서로 다르다는 결론을 내릴 수 밖에 없도록 만든다. 다시 말해, 문자열이 정확히 동일한 문자로 구성된다 하더라도 두 문자열은 동일하지 않다는 말이다! 아래와 같이 그려보라:
다른 동등성 시험에서는 메시지 선택자 =를 이용한다. 이는 정체성보다는 약한 시험이다. 대략적으로 말해, 동등성은 두 객체가 "구별이 불가한지(indistinguishable)"만을 평가한다. "구별 불가"의 정확한 의미는 프로그래머가 주어진 클래스에 대해 그것을 어떻게 정의하는지에 따라 달려 있다; 즉, 프로그래머가 = 메서드를 어떻게 오버라이드하는지에 따라 좌우된다는 말이다.
❏ 아래 두 행을 한 번에 하나씩 실행한 후 마지막 행을 표시하라:
X := 'Hobbes'.
Y := 'Hobbes'.
X = Y.
우리는 두 개의 문자열이 같은 동일한(identical) 문자열 객체가 아님을 목격했지만 위를 표시한 결과값인 true 는 두 개의 문자열이 서로 동일(equal)하다고 말해준다. 따라서 객체는 똑같은 객체일 필요 없이 동등할-구별 불가-수 있다. 반면 동일한 객체는 필연적으로 동등해야 한다.
정체성은, 당신이 == 메서드를 오버라이드할 경우 스몰토크가 당신의 오버라이드를 무시한다는 기본적인 스몰토크적 개념이다.[2] 반대로 = 메서드를 마음대로 오버라이드할 수도 있다. 따라서 자신의 클래스에 동등성의 정의-"구별 불가"-는 전적으로 당신에게 달려 있다. 문제가 되는 사례에서 스몰토크의 디자이너는 문자열의 동등성에 대해 자신만의 정의를 결정할 수 있는데, 즉 두 개의 문자열이 동일한 문자를 동일한 순서로 포함한다면 두 문자열은 동일하다고 결정내릴 수 있단 말이다. 하지만 자신의 클래스에 동등성을 오버라이드하기 전에 다음 절에 나오는 "동등성 오버라이딩"을 읽길 바란다.
정체성 dictionaries
Dictionary 클래스는 그 이름도 유명한 IdentityDictionary 라는 서브클래스를 갖고 있다. 이러한 두 가지 종류의 dictionaries 는 두 개의 키가 "동일한"지 검사하는 데에 서로 다른 검사를 사용한다. 일반 dictionaries 는 동등성 검사를 사용하고, 정체성 dictionaries 는 정체성 검사를 사용한다. 많은 스몰토크 제품에서는 그에 더해 at:put: 과 at: 의 구현부가 Dictionary 클래스보다 IdentityDictionary 클래스에서 더 빠르게 실행된다. 정체성 dictionaries 는 일반 dictionaries 보다 더 적은 저장 공간을 차지한다. 성능이 중요해지면 정체성 dictionaries가 선호될 수 있다. 하지만 동등한(equal) 키가 동일하기(identical)까지 할 경우에만 둘 다 사용할 수 있다. 예를 들어, 정체성 dictionaries에서 문자열을 키로 사용하는 것은 권하지 않는다. 이는 아래와 같은 코드를 작성하는 대부분의 프로그래머들이:
someDictionary at: 'Hobbes' put: '17th century philosopher'.
someDictionary at: 'Hobbes' put: 'stuffed tiger'.
두 번째 at:put: 이 'Hobbes' 에 대한 (유일한) dictionary 엔트리에 '17th century philosopher' 를 'stuffed tiger' 로 대체할 것으로 기대하기 때문이다. 일반 dictionaries는 사실 이러한 방식으로 행동한다. 하지만 dictionary가 정체성 dictionary라면 두 개의 문자열이 구분된 객체이며, 그에 따라 예상과 다르게 구분된 두 번째 엔트리가 생성될 것임을 알고 있다.
요약하자면, 실용적으로 사용되는 거의 대부분의 정체성 dictionaries는 작은 정수이거나 부호로 된 키를 갖는다. 대부분 객체 유형의 경우 동등성은 정체성을 암시하므로 이것이 정체성 dictionaries에 적절한 키가 된다. 대부분 애플리케이션에서는 일반적인 dictionaries로 충분하며, 어떤 유형의 키에서든 신뢰성 있게 작동하는 이점을 가진다.
연습: 정체성 dictionaries
❏ 행별로 실행된 이 코드가 왜 walkback을 생성하는지 설명해보라:
X := IdentityDictionary new.
X at: 'Heidegger' put: 'Difficult existentialist'.
X at: 'Heidegger'.
동등성 오버라이딩
개발자들이 자신만의 클래스에서 원하는 대로 = 메서드를 오버라이드하는 특권을 가진다는 사실을 봐왔다 (74 페이지). 하지만 그러한 사람들은 자신의 프로그램에 미묘한 버그가 생성되는 위험이 있다. 예를 들어, 그들이 세트(set)에 추가하는 객체들이 후에 나타나지 않기도 한다. 필자는 이를 "사라지는 요소의 변칙"이라 부르는데, 다음 절에서 이를 이용한 실험을 해볼 것이다.
변칙은 다음과 같이 발생한다: 세트는 해싱을 이용해 추가된 객체를 어디에 삽입할 것인지 결정하고, 기본 해싱 알고리즘은 두 개의 구분된 (비동일한) 객체에 대해 다른 해시 값을 생성한다. 따라서 세트는 비동일한 객체들을 다른 위치에 놓는 경향이 있는데, 이는 보통 바람직하고 무해하다. 하지만 개발자가 = 메서드를 오버라이드하여 두 개의 구분된 객체가 동등한데, 첫 번째 객체를 세트로 추가한 후 두 번째 동등한 객체가 세트에 있기를 기대하면서 검색했으나 사실 존재하지 않음을 발견할 것이다. 개발자의 오류는 두 번째 객체가 첫 번째와 사실 동등할 뿐인데 동일한 것처럼 행위하도록 기대했다는 점이다.
이 시나리오는 가능성이 적어 보이지만 객체 자체를 대신해 객체에 대해 보통 프록시(proxy)를 사용하는 클라이언트/서버 시스템에서 발생한다. 어떤 프록시가 어떤 객체를 의미하는지 결정하기 위해 시스템은 프록시와 객체에서 문제와 관련된 자료를 (사회보장번호 또는 기타 유일한 식별자) 비교하는 오버라이드된 동등성 검사(overridden equality test)를 사용한다. 프록시와 그 객체는 동일하진 않지만 동등하다고 할 수 있는데, 동일하게 식별되는 자료를 갖고 있기 때문이다. 그러면 시스템은 프록시와 그 객체를 동일한 것처럼 취급하는데, 이는 동등성을 오버라이딩한 목적이다. 하지만 시스템을 "사라지는 요소의 변칙"에 노출하기도 한다. 프록시와 프록시된 객체의 서로 다른 해시 결과는 두 객체에 대해 다른 위치를 명시한다:
이러한 변칙을 피하기 위해서는 클래스에 대해 = 메서드를 오버라이드 할 때마다 hash 메서드도 오버라이드하면 된다. = 메서드가 무엇을 비교하든 hash 메서드를 작성하여 이가 동일한 것을 해시하도록 한다. 위의 클라이언트/서버 예제에서 = 메서드가 사회보장번호를 비교한다면, hash 메서드 또한 작성하여 사회보장번호도 해시하도록 하라. 그러면 세트가 객체의 위치를 결정하기 위해 해시할 때 프록시와 그 객체에 모두 동일한 위치를 계산하도록 확보할 것이다.
연습: 사라지는 요소의 변칙
❏ 인스턴스 변수 isbn 과 단순히 인스턴스 변수를 설정하고 그에 답하는 메서드 setIsbn: 과 getIsbn 으로 구성된 Object 의 서브클래스 Book 을 작성하라:
= anotherBook
^self getIsbn = anotherBook getIsbn
아래 코드는 초기 수용량이 100개의 소장물(holding)을 가진 라이브러리를 구성하여, 소장물을 추가한 후 소장물에 대한 라이브러리를 검사할 것이다. 첫 두 행을 한 번에 한 행씩 실행하고 마지막 행은 표시하라:
Library := Set new: 100.
Library add: (Book new setIsbn: '0-671-20158-1').
Library includes: (Book new setIsbn: '0-671-20158-1').
결과 false는 사라지는 요소의 변칙을 보여준다. (세트는 결과가 true가 될 가능성이 통계학적으로 거의 없을 만큼 크지만, 만일 true라면 세트의 크기를 조정하라.) 이제 Book 클래스를 수정하여 변칙이 발생하지 않도록 하라.
❏ 앞의 실습을 해결했다면 여기 추가 문제가 있다. 객체의 정체성은 때때로 변할지도 모른다. 아마 book이 다른 ISBN 번호로 재할당되었을지도 모른다. 이러한 변화는 라이브러리를 통한 추후 검색에 영향을 미친다: 책을 다시 찾을 수 없을 것이다. 이유가 뭘까?
연습: Streams로의 익스커션(excursion)
Stream 클래스는 공식적으로는 스몰토크의 Collection 계층구조에 있지 않지만 스트림은 컬렉션과 밀접하게 관련이 있으며, 이번 장에서 소개하기엔 민감하다. 스트림은 컬렉션이 할 수 없는 일을 한다: Stream 은 그것이 어디 있었는지 기억한다. 한 장소에서 작업을 하다가 잠시 작업을 멈춘 후에 다시 돌아오면, 떠나기 전과 동일한 장소에서 작업을 계속할 수 있다. 예를 들어, 문자열에서 스트림은 그 문자들 중 당신이 마지막으로 접근한 위치를 기억하여 언제건 스트림에게 next 문자를 요청할 수 있다. 컬렉션은 마지막으로 발생한 장소를 기억할 수 없다.
❏ 아래 코드를 표시한 결과를 예측하라:
ㅣstreamㅣ
stream := ReadStream on: 'Van Gosh'.
stream next; next; next; next; next; next.
스트림은 next 메시지들 간 위치를 기억한다는 사실을 주목하라.
❏ 일상 생활에 직면하는 가장 일반적인 프로그래밍 문제 중 하나는 정보의 다양한 소스로부터 문자열을 구성한 후 그것을 어떤 먼 객체에게 argument로서 전달하는 것이다. 아래 코드 시퀀스를 완료하되:
ㅣstreamㅣ
string := "You write a few lines".
Transcript show: string.
시퀀스를 실행하면 transcript에 아래의 텍스트가 생성되도록 하라:
Sunflowers
Irises
Starry Night
힌트: 당신의 문자열이 다음 행으로 가길 원한다면 키보드 <enter> 키를 사용해 직접 이동시키는 방법 외에는 선택의 여지가 없다. 불행히도 그렇게 이동된 코드는 읽기나 유지하기가 거북하다.
스트림을 생성하고 그것이 완료될 때까지 조금씩 추가시키는 방법이 훨씬 낫다. 점진 처리(incremental processing)의 경우 스트림은 성능이 뛰어나고(excel) 문자열은 실패한다(founder). 문자열의 길이가 고정되어 있으므로 문자열의 증가하는 특성에 상반된다.
❏ 이러한 접근법을 보이기 위해 아래 코드 시퀀스를 완료하여 위의 결과를 재생성하라:
ㅣstreamㅣ
stream := ReadWriteStream on: ' '.
stream cr;
nextPutAll: 'Sunflowers';
"You write a few messages".
Transcript show: stream contents.
stream close.
요약하자면, 컬렉션의 연속 요소로 접근하거나 연속 요소를 추가해야 하는 경우, 위치적 정보를 추적하는 코드를 작성해야 할 것이다. 책임(responsibility)을 행위로 흡수시킴으로써 이러한 의무를 덜어주는 스트림을 사용하는 편이 낫다.
컨테이너 대 집합
집합(aggregation)과 컨테이너는 한 가지 중요한 특징을 공유한다: 크기가 클수록 작은 것을 포함한다는 사실이다. 집합은 본래 객체와 그 부분들 간의 엄격한 관계를 의미한다는 점에서 다르다. 반대로 컨테이너의 구성은 시간이 지나면서 residence를 차지하고 떠나는 요소들과 함께 발전하는 것으로 기대된다. 핸드셋, 다이얼 등으로 구성된 전화기는 집합에 해당한다. 또 낱말 분석기, 파서, 코드 생성기로 구성되는 컴파일러도 마찬가지다. 반면, 세트 또는 큐는 컨테이너에 속하며, 이들은 내부에서 객체의 coming과 going을 주요 책임으로 한다. 따라서 컨테이너는 요청을 추가하고 제거하기 위해 응답해야 한다. 이러한 프로퍼티는 스몰토크의 Collection 계층구조를 브라우징하여 모든 add: 와 remove: 메서드를 주목하여 확인할 수 있다.[3] 추가(add)나 제거(remove)보다는 픽업(pickup)과 다이얼(dial)이 더 명백한 행위인 telephone과 같은 객체는 컨테이너로서 설계되어선 안 된다.
문제는 이러한 명백한 차이가 때로는 그렇게 분명하지 않다는 점이다. 객체가 집합의 경직성을 가지는지, 혹은 컨테이너의 유연성을 가지는지는 정도의 차이에 불과하다. Branch 위의 leaves을 생각해보라. Leaves는 branch 위에 고정된 위치에 완고하게 위치하면서 매년 왔다가 다시 간다. 혹자는 branch가 집합이라고, 혹자는 컨테이너라고 주장할 수 있겠다.
많은 경우에서 객체는 주로 집합이지만, containment 프로퍼티도 가진다. 인간의 몸은 사지와 장기들의 집합이면서 계속적으로 보충되는 혈구를 포함한다. 그렇다고 신체가 컨테이너가 되는 것은 아니다-Collection 의 서브클래스로 정의하진 않을테니 말이다. 그것은 집합이지만 집합의 구성요소들(components) 중 하나가 어쩌다보니 컨테이너, 즉 혈구의 컨테이너인 circulatorySystem 일 뿐이다. 신체 자체보다는 신체의 구성요소가 컨테이너다.
이와 유사하게 클래스 브라우저는 주로 사용자 인터페이스 위젯의 집합이긴 하지만 그것이 보여주는 클래스에 대한 메서드의 집합을 포함하기도 한다. 다시 말하지만, 브라우저 자체는 컨테이너는 아니지만 그 구성요소들 중 하나가-그것의 methods 인스턴스 변수-메서드를 포함하는 SortedCollection 의 인스턴스다.
이러한 분해(decomposition)와 닮은 디자인은 셀수 없이 많다. 신체와 순환기관, 브라우저와 그 메서드, 혹은 냉장고와 채소칸은 모두 다수의 서브 객체를 가진 객체들로, 이 서브 객체 중 하나 또는 그 이상은 container 객체다. 이러한 디자인의 예를 본 책에 걸쳐 여러 개 목격할 것이다; 18장의 디자인 패턴 smart container 를 나타낸다.
얕은 복사와 깊은 복사
객체를 복사하는 일은 간단하게 들린다. 사실 이는 객체 지향 프로그래밍에서 가장 미묘하고 가장 문제가 발생하기 쉬운 영역 중 하나이다. 객체가 다른 객체들을 참조할 때는 본질적인 질문이 발생한다. 컨테이너를 생각해보자. 컨테이너를 복사하면 그 안의 요소에는 무슨 일이 발생할까? 좀 더 간단한 예제에서, 하나의 객체가 두 번째 객체를 가리키는 인스턴스 변수 v를 갖고 있는데, 첫 번째 객체를 복사한다고 가정하자. 두 번째 객체엔 어떤 일이 발생할까?
질문에 두 가지 답이 있다. 요소 또는 참조된 객체도 복사된다면 복사는 깊다; 그렇지 않고 공유만 된다면 얕은 복사이다.
그림에서 볼 수 있듯이 얕은 복사는 참조된 객체를 공유하고자 할 때 적절하다. Container 객체들은 주로 얕게 복사되는데, 그들의 내용을 공유하려는 경우가 많기 때문이다. 사실 얕은 복사가 스몰토크에서 많이 사용된다 (그리고 기본 복사에 해당한다). 예를 들어, 부동산업자는 자신과 다른 사람들이 모두 동일하게 공유된 엔트리를 수정할 수 있도록 국가의 부동산 리스트 컬렉션의 얕은 복사를 선호할 것이다. (어떤 유형의 복사보다도 master collection 자체로 접근하면 더 행복해할 것인데, 그렇게 하면 삭제(deletion)와 새 리스트를 유지할 수 있기 때문이다.) 깊은 복사는 일부 상황에서 적절하다. Xerox copy는 깊은 복사인데, 복사본과 원본이 어떤 것도 공유하지 않기 때문이다. 복사는 원본의 완전한 복제다. 깊은 복사는 또 집합에-예를 들어 집과 주차장 또는 자전거와 그 모든 부품, 재귀적으로는 그 모든 하위부품까지-적절하기도 하다. 두 개의 집이 하나의 주차장을 공유해선 안 되며, 두 개의 자전거가 하나의 좌석을 공유해서도 안 된다. 집합은 일반적으로 그 구성요소와 엄격한 연관(rigid association)을 유지한다.
하지만 깊은 복사와 얕은 복사 사이에 애매한 영역(gray area)이 있다. 집에 그 건축업자를 위한 인스턴스 변수가 있는데, 집의 복사본이 동일한 건축업자를 공유하길 원한다고 가정하자. 그러면 집의 복사본은 얕은 특징과 깊은 특징을 모두 가져야 한다. 건축업자를 공유하기 위해서는 얕은 복사를, 주차장을 비롯해 부엌과 다른 방들을 복사하기 위해서는 깊은 복사가 필요하다. 이 예제는 객체들이 복사될 가능성이 높은 클래스를 개발하면 객체의 인스턴스 변수에 대한 공유 요구조건에 따라 그 복사 방법을 설계해야 함을 보여준다.
논평: 값과 참조 의미구조
스몰토크는 참조 의미구조(semantics)를 기반으로 한다. 그 프로그래밍 모델은 하나의 객체를 (또는 변수를) 다른 객체로 (또는 다른 변수로) 참조하는 포인터에 의존한다. 스몰토크에서는 하나의 seat 객체가 삽입된 bicycle 객체를 생각할지 모르지만 bicycle 객체는 seat 객체를 참조하는 포인터만 가질 뿐이다. 따라서 메시지가 argument 객체를 전달할 때는 사실 그러한 객체들로 포인터를 전달하는 것이다. (기계적 수준에서 보면, 문자나 작은 정수와 같은 작은 객체들만 온전히 전달되고, 사실은 큰 객체에 의해 참조되기보다는 큰 객체에 삽입된다. 하지만 스몰토크 프로그래머의 개념적 관점은 포인터를 기반으로 한다; 즉 참조 의미구조를 의미한다.)
반면 C++는 참조 의미구조 뿐만 아니라 값 의미구조도 지원한다. C++ 프로그래머는 선택적으로 seat을 계속해서 bicycle로 삽입할 수도 있다. 또한 메시지가 객체를 향한 포인터 대신 객체의 복사본을 전달함으로써 객체를 전달하는 경우도 있다. 객체를 복사하는 C++ 코드의 이러한 경향 때문에 C++ 프로그래머들은 스스로 깊은 복사를 할 것인지 얕은 복사를 할 것인지를 엄격히 생각하도록 훈련해야 한다. 각 클래스에는 프로그래머가 단순히 그 인스턴스가 어떻게 작동하는지 정의하는 특별한 특징이 (복사 생성자) 있기 때문에 C++에서 복사는 엄청난 집중을 요한다. 스몰토크 프로그래머들은 복사 행위에 관한 사고의 훈련이 덜 된 경향이 있으며, 스몰토크에서 복사가 발생하는 경우가 훨씬 적기 때문에 교묘하게 피해간다.
논평: C++에서 컨테이너
C++ 컨테이너는 링 바인더와 같다: 일반적으로 하나의 클래스로부터 (또는 그 클래스의 서브클래스) 객체를 보유한다. 이러한 컨테이너는 스몰토크의 것보단 덜 유연하지만 더 안전하며, 올바르지 않은 객체를 C++ 컨테이너로 추가하려는 코드는 컴파일이 실패하게 되어 있다. 이러한 안전성을 위해서는 자신이 보유하고자 하는 종류의 객체에-SetOfWhale 을 비롯해 SetOfInteger-전문화된 컨테이너의 클래스를 개발할 필요가 있다. C++ 언어적 특징은 (templates) 그러한 container 클래스의 정의를 간소화하지만, C++에서 container 라이브러리는 여전히 통제하기 힘들고, 복잡하며, 작성하기 어렵다. 빠르고 탄탄한 컨테이너의 필요성이 container 라이브러리의 가내공업(cottage industry)을 야기해왔다. 때로는 이러한 라이브러리를 기초(foundation) 라이브러리라고 부르며 객체 프로그래밍에서 그들의 본질적인 위치를 인정하기도 한다. 아쉽게도 기초 라이브러리는 종종 윈도잉이나 통신이나 지속성과 같은 다른 서비스를 제공하는 더 큰 프레임워크나 라이브러리로 통합되므로 대체가 불가능하다.4 반면, 모든 스몰토크 dialect는 통합된 기반 라이브러리-Collection 의 서브클래스-를 포함한다. 이 라이브러리는 스모롵크에서 분리할 수 없는데 스몰토크 자체의 대부분이 collection 클래스를 이용해 빌드되기 때문이다.
Notes
- ↑ IBM 을 포함해 일부 Smalltalk dialect 에서 이와 같은 예제의 행위는 코드가 한 번에 컴파일되었는지, 아니면 조각별로 컴파일되었는지에 따라 좌우된다. 한 번에 컴파일된 경우, 컴파일러는 우리가 보고 싶어하는 놀라운 결과를 위장한 최적화(optimizations)를 실행할 수 있다.
- ↑ 사실 자신만의 == 메서드의 실행을 강요하는 흔하지 않은 방법이 있다. 197페이지 technical aside를 참고하라.
- ↑ 일부 dialect에서 add:와 remove:는 Collection 내 순수 가상 (subclassResponsibility) 메서드로, 서브클래스가 추가와 제거를 지원해야 할 의무가 있음을 상기시킨다.