SmalltalkObjectsandDesign:Chapter 09

From 흡혈양파의 번역工房
Jump to navigation Jump to search
제 9 장 상속을 해야 (하지 말아야) 할 때

상속을 해야 (하지 말아야) 할 때

스몰토크 코드를 어느 정도 작업했으니 이제 개념적 도전과제로 돌아가자. 제 2장에서 필자는 집합과 상속이 독립적이고 구분된 개념이라고 제시한 바 있다. 사실은 이 둘은 그다지 독립적이진 않으며, 특정 문제에 대해서는 둘 중에 어떤 것을 적용해야 할지 결정하기가 힘들다. 이번 장에서는 둘 사이의 차이를 소개하고자 한다. (추가 논의는 제 14장 다형성에서 소개하며, 255 페이지의 논평에서도 제공한다.)


역사적 배경

최근 몇 년간 상속은 집합에 비해 엄청난 관심을 받아왔으며, 그 중 대부분은 방식(fashion)과 관련되었다. 집합은 프로그래밍 생에서 오래된 측면에 속한다. 프로그래머들은 수십 년간 그 개념을 사용해왔으므로 멋진 이름을 붙이는 수고를 들일 필요를 느끼지 못했다. Pascal에서 집합은 다음과 같은 모양을 한다:

type Flight = record
                        gate: integer;
                        terminal: char;
                        onTime: boolean
                     END;


그리고 C에선 다음과 같다:

struct Flight {
                int gate;
                char terminal;
                int onTime;
         };


두 예제 모두 세 개의 구성요소로부터 airline flight을 구성하는 개념을 표현한다. 물론 최근에는 flight와 그 구성요소들이 모두 객체일 가능성이 크지만, 기본이 되는 개념은 여전히 집합(aggregation)이다. 스몰토크에서는 아래와 같이 작성할 수 있겠다:

Object subclass: #Flight
          instanceVariableNames: 'gate terminal onTime'
          ...


스몰토크 코드 단편에 타입 정보가 빠진 것을 주목하라. 이러한 부재는 일반적인 객체 지향 프로그래밍의 특성은 아니지만 스몰토크의 특성은 확실하다. C++ 버전은 타입 정보를 포함해 C 버전과 많이 닮았지만 struct 라는 단어 대신 class 를 사용한다는 중요한 차이점이 있다.


계층구조 역전시키기

어휘에서 상속뿐 아니라 집합이 있는 경우 디자인의 잠재적 복잡성은 두 배가 된다. 언어가 힘을 얻게 되면 혼동의 기회도 발생한다. 아래와 같이 매우 간단한 집합을 예로 들어보자:

Chapter 09 01.png


이 집합을 역전시켜 적합한 상속 계층구조를 생성할 것을 권한다:

Chapter 09 02.png


필자의 주장은 다음과 같다. 제 2장에서 상속에 관한 논의에 따라 우리는 서브클래스의 인스턴스가 슈퍼클래스의 인스턴스보다 더 많은 프로퍼티를 가진다는 경험 법칙에 동의했다. 그렇다면 입(mouth)은 치아, 잇몸, 입술로 꾸며진 혀(tongue)이므로 MouthTongue 의 특별한 유형 또는 서브클래스에 속해야 한다. 이보다 더 충격적인 것은 얼굴(face)은 입(mouth) 외에도 눈, 코, 귀, 볼과 같이 많은 추가 프로퍼티가 합해지기 때문에 FaceMouth 의 특별한 유형 또는 서브클래스여야 한다는 점이다. 얼굴은 입이 하는 모든 일을 (먹는 것) 하며, 보기도 하고, 냄새도 맡고, 소리도 듣는다. 터무니없이 들리겠지만 이러한 분석은 상속의 정의와 완전히 일치한다. 스몰토크나 C++에서 이러한 방식으로 클래스 계층구조를 디자인하고 구현하는 데에 방해가 되는 것은 아무 것도 없다.


이런 이유로 딜레마가 발생한다: 이러한 클래스들은 집합을 이용해 디자인해야 할까, 상속을 이용해 디자인해야 할까? 이번 경우는 자신의 직관을 믿도록 한다. 상속을 사용하는 것이 전적으로 가능하긴 하지만 FaceMouth 의 특별한 유형으로 생각하는 것은 직관적이지 않다. 그리고 디자이너가 이해하기 어려운 내용을 디자인을 사용할 다른 프로그래머들이 쉽게 이해할리는 만무하다. 객체의 프로그래밍에 있어 핵심은 그들이 약속한 인지 경제성이다; 훌륭한 디자인은 정신적 번역(mental translation)을 감소시켜준다. 이러한 관찰만으로도 혀를 입의 일부로, 입을 얼굴의 일부로 디자인하는 것을 정당화한다.


그렇지만 아직은 이 예제를 그만둘 준비가 되지 않았다. 이러한 결정이 다른 언어에서는 이토록 명확하지 않을 수도 있다. 스몰토크는 단일 상속-각 클래스가 정확히 하나의 직속(immediate) 슈퍼클래스를 가진다-을 지원한다. C++ 를 비롯해 다른 언어들은 하나의 클래스가 여러 개의 직속 슈퍼클래스로부터 상속받을 수 있는 다중 상속을 지원한다. 이러한 언어들을 사용하는 디자이너들은 때때로 mix-ins 라 불리는 기법을 이용한다. Mix-ins 는 Face 와 같은 좀 더 복잡한 서브클래스를 생성하기 위해 슈퍼클래스로서 사용되는 Mouth 또는 Nose 와 같은 간단한 클래스를 의미한다. 서브클래스는 다수의 mix-in 슈퍼클래스로부터 상속을 통해 프로퍼티와 행위를 원하는대로 얻는다. 따라서 Face 는 Mouth 와 Nose 둘 다로부터 상속받을 수 있다. Mix-ins는 분명히 상속 메커니즘에 의존하지만 디자이너는 구성(composition) 또는 집합을 생각하고 있음을-mouth와 nose와 같은 구성요소(component)로부터 face와 같은 집합(aggregate) 객체를 빌드하는 것-주목한다.


구매하느냐buying, 상속하느냐inherit?

앞 절의 문제는 구매하느냐buying, 상속하느냐inheriting라는, 가장 특징적인 객체 지향 디자인의 진퇴양난 중 하나를 설명한다. 객체를 구매하는(buying) 것은 종종 집합이 사용하도록 하나를 얻는 것을 의미한다. 구매란 단순하고 암시적인 단어로, Bertrand Meyer가 [Meyer 1988] 프로그래밍 객체의 맥락에서 제일 처음 사용한 것으로 알고 있다. (좀 더 형식적이지만 동의어에 해당하는 compose 를 사용해도 좋다.)


위에서 선호되는 디자인에서 Face 클래스는 Mouth 클래스를 구매한다. 스몰토크에서는 Mouth 객체를 참조하게 될 인스턴스 변수를 Face 클래스에 정의함으로써 구현할 수 있다:

Object subclass: #Face
          instanceVariableNames: 'mouth ...'
          ...


Face 를 초기화하는 메서드는 아래와 같은 문을 포함할 것이다:

mouth :=Mouth new.


상속하길 원하는 mouth를 구매하는 대신 좀 덜 바람직한 디자인이라면 아래와 같을 것이다:

Mouth subclass: #Face
        ...


이 두 가지 디자인은 디자인이 이용할 수 있는 선택권의 전체 범위를 나타낸다. 객체 지향 디자이너가 클래스 행위 또는 프로퍼티에 접근하기 위해 사용할 수 있는 기법에는 두 가지가 있다: 구매 또는 상속. Y 클래스에 보이는 내용이 마음에 들어서 그것을 X 클래스로 통합하길 원할 경우, Y 로부터 구매하거나 Y 로부터 상속해야 한다; 이 두 가지 방법은 X 에게 Y 로의 직접적 접근성을 제공하는 유일한 방법이다. (간접적 관계는 완전히 다른 문제다: 다대다(many-to-many) 관계에 관한 논의는 217 페이지를, lawyer 객체에 관한 논의는 233 페이지를 참고한다.)


이것은 대담한 주장이지만 당신의 경험과 전적으로 일치한다. 당신이 디자인하거나 목격한 모든 직접적 관계는 구매하거나 상속한다. 예를 들어, 제 7장과 8장의 checking account 실습에서 Account 클래스는 로그로 사용하기 위해 SortedCollection구매한 것이다.


연습

스몰토크에서 Collection 클래스는 추상 클래스로서, 컨테이너 같은(container-like) 프로퍼티들을 가진 다수의 서브클래스의 슈퍼클래스라는 점을 기억하라. 이러한 서브클래스들 중 하나가 바로 OrderedCollection 이다. OrderedCollection 의 인스턴스는 그 요소들을 상대적 위치에서 유지시킨다: 첫 번째, 두 번째, …마지막 요소를 갖는다. 이는 양끝 중 하나로부터 제거하거나 추가하는 메서드를 갖고 있다. 따라서 크기가 증가 또는 축소 가능한 배열(array)의 모습과 비슷하다. 이 연습을 위해서는 먼 끝, addLast:removeLast 로부터 축소 또는 연장되는 메서드에 관심을 가져야 한다.


Stack 클래스를 디자인하라. OrderedCollection 클래스는 이미 스택과 같은 프로퍼티를 제공하기 때문에 당신은 OrderedCollection 을 직접 활용하길 원할 것이다. OrderedCollection 으로부터 구매를 이용한 해답을 그림으로 그린 후 상속을 이용한 해답을 그림으로 그려라.


해결책과 논의

정렬된 컬렉션을 구매하는 해결책은 다음과 같다:

Chapter 09 03.png


인스턴스 변수 oc 는 스택으로 하여금 그것이 구매하는 정렬된 컬렉션으로 접근할 수 있도록 해준다. 스택에 꼭 필요한 메서드로 push:pop 이 있다. 그들의 코드는 단순히 인스턴스 변수에게 요청을 보내고 정렬된 컬렉션으로부터 적절한 메서드를 구매한다. 따라서 푸싱(pushing)의 경우 아래가 되고:

push: anObject
   oc addLast: anObject


그리고 팝핑(popping)의 경우 다음이 된다:

pop
   oc isEmpty
          ifTrue: [ ^nil ].
   oc removeLast


(첫 번째 문장은 누군가 빈 스택으로부터 팝(pop)을 시도하는 경우 walkback을 막는다.) 초기화(initialization)는 인스턴스 변수가 유효한 정렬된 컬렉션을 가리키도록 준비시킨다:

initialize
   oc := OrderedCollection new


이를 OrderedCollection 클래스로부터 구매하는 대신 상속하는 디자인으로 바꾸라:

Chapter 09 04.png


구매하는 것이 아니기 때문에 요청을 전달할 인스턴스 변수가 없다. 그리고 상속 중이므로 스택은 그 슈퍼클래스로부터 addLast:removeLast 메서드를 상속받는다. 스택이 바로 정렬된 컬렉션이다; 그것이 바로 상속의 의미하는 바이다. 그림에 하나의 객체만 표시된 이유이다. 따라서 push: 에 대한 코드는 다음과 같다:

push: anObject
   self addLast: anObject


구매와 유일하게 다른 점은 selfoc 대신 메시지를 수신한다는 점이다. 사실, self 는 메시지를 수신하는 데에 이용할 수 있는 유일한 객체이다.


pop 에 대한 코드도 동일하게 소규모로 변한다. 그리고 initialize 메서드는 사라지는데, 그것을 준비시킬 인스턴스 변수가 없기 때문이다.


결론

구매, 상속 중 어떤 기법을 사용하든 작동한다. 일반적으로 상속이 더 작은 해결책을 낸다. 예제에서 상속은 oc 인스턴스 변수뿐만 아니라 initialize 메서드를 생략하도록 해준다. 이 작은 예제에서 순수하게 절약되는 코드는 두 행에 불과하지만 상속이 코드를 줄여준다는 일반적인 규칙은 여전히 지켜주는 셈이다.


상속의 또 다른 결과는 밀접한 결합이다: 상속은 서브클래스를 그 슈퍼클래스로 매우 밀접하게 결합하여 슈퍼클래스로 적용되는 모든 것이 서브클래스에도 적용된다. 이 결합이 바람직한지 아닌지는 상황에 따라 달라진다.


우리 예제에서는 밀접한 결합이 바람직하지 않을지도 모른다. 정렬된 컬렉션에 작동하는 수많은 메시지들은 스택에서도 작동할 것이라는 의미가 함축되어 있을 것이다. 이는 우리가 예상한 것 훨씬 이상이다; 스택이 at:put: 또는 removeFirst 와 같은 OrderedCollection 메시지에 응답하는 것은 신뢰할 수 없다. 신뢰가 가는 스택은 push:pop 과 같은 메시지에만 응답해야 한다. 따라서 스몰토크에서 파괴적인 해결책을 피하기 위해서는 디자이너가 상속 대신 구매를 이용해야 한다. 이것이 바로 첫 번째 해결책이 보여준 내용이다: 구매는 스택이 OrderedCollection 메시지에 응답하지 않도록 보호해준다.


상속, 그로 인해 밀접하게 결합된 두 개의 클래스는 장기간 책무(commitment)에 해당하는데, 소프트웨어가 세월이 지나고 유지보수 및 개선을 경험하면서 슈퍼클래스에 일어나는 어떤 변경내용이든 자동으로 서브클래스에 반영될 것이기 때문이다. 다시 말하지만, 이러한 헌신이 바람직한지 아닌지는 상황에 따라 달라진다. 디자이너는 클래스의 결합에 대한 경제성뿐만 아니라 클래스의 사용자들이 클래스의 발달을 기대하는지의 여부도 고려해야 한다. 슈퍼클래스에서 후에 추가된 public 메서드들이 서브클래스에도 연관될 것으로 기대하는가?


우리 예제에서는 스택에 있어 OrderedCollection 의 public 프로토콜에 대한 개선이 중요해질 가능성은 거의 없다. 스택은 push와 pop 외에 그다지 많은 일을 수행해선 안 된다. 따라서 OrderedCollection 으로부터 Stack 을 서브클래싱할 경우, 어떠한 유지보수 혜택도 부여하지 않으며, 심지어 스택의 사용자에게 정렬된 컬렉션과의 유사점을 필요 이상으로 기대하도록 속일지도 모른다.


스택을 대상으로 한 연습에서 소프트웨어 공학과 관련된 고려 사항들은 구매(buying)에 도움이 된다. OrderedCollection 으로부터 Stack 을 상속하는 데에서 절약하는 코드의 양은 미비하므로 신뢰성과 유지보수 영향을 해칠만큼의 가치는 없다.


구매 대 상속의 결정은 객체 지향 디자인에서 기본적인 활동이다. 주로 초기 직관이 타당하지만 균형(trade-off)에도 중점을 두어야 한다-(1) 코드 감소, (2) 파괴적 슈퍼클래스 메시지, (3) 유지보수-아래 표에 요약하겠다:

  구매하기
(다른 클래스의 메서드에 선택적으로 접근)
상속하기
(다른 클래스의 모든 메서드와 인스턴스 변수로 접근)
코드량(Code bulk) 더 많음 더 적음 (바람직!)
파괴적 슈퍼클래스 메시지 예방 가능 (바람직!) 가능
유지보수 독립적으로 발전 (느슨한 결합) 함께 발전 (단단한 결합)
슈퍼클래스 내부와의 친숙도 더 적음 더 많음


표의 마지막 행은 개발자가 상속이나 구매를 하기 전에 클래스를 연구해야 하는 수준을 나타낸다. 구매에는 상속에 비해 내부와 낮은 친숙도를 요구한다. 이러한 이유로 인해 구매는 때때로 블랙 박스 재사용이라 부르고, 상속은 화이트 박스 재사용이라 부른다.


그렇다고 해서 이 표가 구매 대 상속 딜레마의 최종적 의견은 아니다. 상속을 이용해야 하는 좀 더 설득력 있는 이유는 제 14장, 다형성에서 논하겠다. 255 페이지의 논평에서도 객체 지향 프레임워크의 맥락에서 구매 또는 상속 결정을 요약한다.


다른 객체 지향 언어에서의 상황은 표에서 제시하는 것만큼 명확하지 않을 수도 있다. C++ 디자이너들은 스몰토크 디자이너에 비해 상속을 더 많이 사용하는데, 다형성을 표현하는 유일한 수단이라는 것이 부분적인 이유가 되겠지만, C++ 언어에는 슈퍼클래스의 대규모(wholesale) 상속 특징을 제한할 수 있는 기능들이 있기 때문이기도 하다. 스몰토크에는 그러한 기능이 없기 때문에 스몰토크 디자이너들은 위의 주의사항을 명심해야 한다.


불확실한 상황이라면 구매를 고려하라. 초보 디자이너들은 상속을 남용하는 경향이 있다; 숙련된 디자이너들은 구매 방법을 시도하려고 노력한다. 경험으로 보건대, 구매가 상속보다 덜 불안정하다.


논평: 다중 상속

다중 상속(99 페이지)은 일부 객체 지향 언어에서 강력한 기능에 속한다. 그렇다면 자신의 언어에서 (스몰토크) 다중 상속을 지원하지 않는다면 어떻게 할 것인가? 이번 장에 설명된 원칙에 따르면 한 가지 선택, 구매만이 남는다. X와 Y 두 개의 클래스로부터 C 클래스가 상속받길 원할 경우, 스몰토크는 그 중 하나로부터 하나씩만 상속하도록 제한하므로, 당신은 구매를 해야 할 것이다.


문제는 X와 Y 둘 다로부터 상속을 받길 원한다는 점이다. 즉, C가 X이면서 Y인 것처럼 행위하길 원할 수 있는데, 이는 C가 X나 Y와 같이 모든 메시지로 자동으로 응답해야 함을 의미한다. 상속은 이러한 자동 프로퍼티를 갖는다. 하지만 구매는 그렇지 않다. 따라서 이러한 상황에서는 구매라는 대체방법이 바람직하지 못하게 된다. 스몰토크는 C++ 프로그래머를 만족시킬 만한 수준의 다중 상속을 시뮬레이트하는 데에 능숙하지 않다.[1]


다중 상속에 관한 논의가 완전히 다중 상속의 장점으로 편향되는 것은 아니다. 다중 상속의 가치를 깎아내리는 이들은 다중 상속이 해결하기 복잡한 언어적 규칙을 요하는 문제를 야기하고, 이러한 당혹스러운 문제들이 다중 상속의 이익을 상쇄시킬만큼 흔히 발생한다고 주장한다. 여기서는 반복 상속(repeated inheritance)라고 알려진 문제를 소개할 것인데, 이는 클래스가 다른 클래스로부터 한 번 이상 상속받기 때문에 붙여진 이름이다.


Chapter 09 05.png

특정 텔레비전 프로그램들은 역사적 문제의 극화(dramatization)와 기록성 다큐멘터리 장면을 결합하여 다큐드라마(docudramas)를 생성한다. 이러한 상황에 대한 다중 상속 계층구조를 우측에 표시하겠다.


TVShow 클래스 내에 쇼의 director 에 대한 인스턴스 변수를 생각해보자. DramaDocumentary 클래스는 명백하게 이러한 인스턴스 변수를 상속한다. 따라서 DocuDrama 클래스는 각 슈퍼클래스로부터 하나씩, 총 두 개의 directors 를 상속하기 위해 대기하는데, 그 모습이 미적으로는 형편없을 것이며, DocuDrama 가 하나의 director 인스턴스 변수만 상속하는 편이 나을 것이다.


반면, 또 다른 TVShow 인스턴스 변수인 duration 을 생각해보자. DramaDocumentary 클래스는 다시 이 인스턴스 변수를 상속하지만, 이제는 DocuDrama 가 인스턴스 변수에 대한 두 개의 복사본을 상속하여 극적인 소재의 분(minutes)과 다큐멘터리 소재의 분(minutes)을 따로 포착할 수 있길 원한다.


각 가능성을 제공하기 위해-다중으로 상속된 몇 가지 인스턴스 변수들을 공유하고 다른 인스턴스 변수들을 복사하는-다중 상속을 지원하는 언어들은 추가로 복잡한 기능을 도입한다. 상세한 내용은 C++의 virtual base classes [Stroustrup 1991]과 Eiffel의 renaming [Meyer 1992]를 참고한다. 짧게 말해, 다중 상속은 프로그래밍 언어에 힘을 더하지만 그로 인한 복잡성은 감수해야 한다.


여기 마지막 역사상 진품이 있다: 다중 상속이 Smalltalk-80 에 잠시 나타났으나 그 혜택이 그로 인해 야기된 복잡성을 충당하기에 충분치 않아 취소된 바 있다.


Notes

  1. 구매가 상속을 대체할 수 없을지도 모르지만, 정반대는 어떤가: 상속이 구매를 대체할 수 있을까? 9.6절에서 논한 이유로 인해 스몰토크에서는 드문 일이다. C++는 private 상속이라는 상속 형태를 지원하는데, 이는 스몰토크의 상속보다는 구매와 훨씬 더 닮았다.