SmalltalkObjectsandDesign:Chapter 17
- 제 17 장 두 가지 상속 유형
두 가지 상속 유형
기초를 벗어난 주제들 중에 다음으로는 타입과 클래스의 차이를 살펴보고자 한다. 지금까지는 이 차이가 모호했다: 상속을 AKO(A-Kind-Of) 개념과 동일하게 생각하라고 부탁한 적이 있을 것이다. 하지만 노련한 디자이너들은 이런 과도한 간소화에 대해 당혹해 한다.
미녀와 야수
아무 객체 지향 환경을 골라 Rectangle과 Square, 두 클래스를 생각해보자. 스몰토크는 걱정하지 마라; 이번 장의 앞부터 시작해 4개 절은 객체 지향 언어와는 무관하다. 여기 rectangle과 square 의 인스턴스가 있다:
각각은 면적을 계산하는 메서드와 그 위치(예: 상단 좌측 모서리)와 변(side)의 길이(들을)를 확인하기 위한 인스턴스 변수들을 갖고 있다. 회전-우리의 모든 squares와 rectangles는 수직면과 수평면만 가질 것이다-은 걱정하지 않는다.
디자인 문제는 다음과 같다: Rectangle은 Square의 슈퍼클래스여야 하는가? 아니면 그 반대여야 하는가? 완벽하게 논리적인 두 가지 의견을 대자면 다음과 같다:
1. Squares는 길이가 동일한 변을 가진 rectangles의 특별한 유형이다. 학생이라면 모두 이 사실을 알고 있다. 따라서 Square는 Rectangle의 서브클래스여야 한다.
2. 우리는 서브클래스가 그 슈퍼클래스에서 모든 것을 상속하고, 일반적으로 추가 특성까지 추가할 것으로 기대한다. 나비는 모든 곤충의 특성을 가지면서 추가 특성까지 지닌다. 위의 그림은 rectangle에 square의 특성들과 하나의 추가 인스턴스 변수까지 더해짐을 분명히 보여준다. 따라서 Rectangle은 Square의 서브클래스여야 한다.
하나의 관점만 집중적으로 사용하기 전에 우선 어떤 주장에도 아무 이상이 없음을 알려주고 싶다. 논쟁은 피할 수 없다: 서브클래싱을 하는 이유에는 두 가지가 있고, 이 둘이 항상 양립되지는 않는다. (누군가 두 가지 이유 모두 납득이 안 된다고 생각하면 둘 중 어떤 클래스도 다른 클래스의 서브클래스가 되지 않도록 디자인할 수 있다; 이 방법도 유효한 세 번째 대안책이다.)
두 명의 객체 지향 프로그래머가 있는데, 한 명은 클래스 계층구조의 소비자(consumer)이고 다른 한 명은 계층구조의 생산자(producer)라고 가정하자. 소비자는 최소한의 혼동으로 계층구조 내 클래스에 도달하여 사용("구매")하길 원한다. 그는 서프라이즈는 원하지 않는다; 계층구조가 직관적인 AKO 계층구조가 될 것이라고 기대하며, 기본이 되는 코드나 인스턴스 변수를 조사하고픈 마음도 없다. 학생들처럼 이 디자이너는 Square가 Rectangleb의 서브클래가 될 것이라 예상한다. 소비자는 외부 일관성을 중요시한다.
반대로 계층구조의 개발자 또는 생산자는 객체의 내부가 어떻게 작동하는지에 관심을 둔다. 그녀는 Square로부터 Rectangle을 서브클래싱하는 이유로 두 가지를 든다. 첫째, 자동으로 position과 side 인스턴스 변수를 상속하고, side2에 대한 인스턴스 변수만 추가하기 때문에 그녀의 일이 단순해진다. (물론 side와 side2의 곱(product)을 계산하기 위해 area 메서드를 오버라이드해야 한다.) 둘째, 다른 방식으로 서브클래싱 할 경우 모든 square가 필요하지 않은 인스턴스 변수까지 (side2) 추가로 상속해야 하는 상황이 올까 염려한다; 그래픽 애플리케이션이 수천 개의 square를 사용할 경우 메모리를 엄청나게 낭비할 것이다. 이 개발자는 실용성을 중시한다.
그렇다면 일관성과 실용성 간 상충관계에서 딜레마가 있다. 외부의 매력적인 모습을 위한 상속과, 내부의 코드 재사용 및 머신 제약을 위한 상속을 동시에 할 수는 없다. 클래스 계층구조는 그 소비자와 생산자를 한 번에 만족시킬 수는 없다.
우리 어휘를 분명히 할 필요가 있다. 외부에서 매력적인("미녀") 첫 번째 유형의 계층구조는 타입 계층구조다. 여기에 중점을 두면 우리는 타입이나 서브타입이란 단어를 사용한다. 일관성을 뜻하는 AKO는 사실상 서브타이핑이다.
내부("야수")를 중시하는 두 번째 계층구조에는 익숙한 단어인 클래스와 서브클래스가 통하며, 구현 상속이란 표현도 자주 사용된다. 물론 실생활에서 "상속" 이라는 단어를 종종 본 적이 있을 것이다. 일반적으로 전문가가 몇 개의 행을 그린 후 "상속"이라고 말하면 그것은 구현 상속을 의미한다. 이번 장에서 혼동의 위험을 최소화하기 위해 문맥이 분명하지 않은 한 "상속"만 사용하는 일은 피할 것이다-"서브타입 상속"이거나 "서브클래스 (또는 구현) 상속," 또는 간단히 "서브타이핑"이나 "서브클래싱"이라 부를 것이다.
객체 공동체는 이렇게 거슬리는 두 개의 상속 형태가 있다는 것을 1986년에 처음 인식하였다. [Snyder 1986; Lalonde et al. 1986] 참고) 문제를 해결하기 위한 노력으로 몇 가지 연상적 동의어가 발생했다:
서브타입 상속 (미녀) | 서브클래스 상속 (야수) |
서브타이핑 | 서브클래싱 |
명세 상속(specification inheritance) | 구현 상속(implementation inheritance) |
시각적 상속 | 비시각적 상속 |
필수 상속 | 부수적 상속(incidental inheritance) |
세부 내용은 [Snyder 1986; Sakkinen 1989; Wegner and Zdonik 1988]을 참고한다. 어떤 이름이든 서브타입/서브클래스의 차이는 객체 지향의 삶에서 피할 수 없는 사실이라는 것이 명확해졌다.
타입이 중요한 이유: 다형성
객체의 타입(미녀; beauty)은 소비자 프로그래머가 어떤 문제에 대한 객체의 적용 가능성을 인지하는 수단이다. S가 T의 서브타입(Butterfly와 Insect 또는 Square와 Rectangle을 상상해보라)이 되기 위해서는 S의 외부 기술(external description)이 T의 외부 기술과 일관성이 있어야 한다. 그리고 일관될 경우, 당신은 프로그램이 프로그램 내에서 T 타입의 무언가를 에상하는 곳이라면 어디서든 S의 인스턴스를 사용할 수 있을 것으로 기대한다. 이러한 프로퍼티를 대체가능성이라 부른다: 서브타입의 인스턴스는 슈퍼타입의 것이 예상되는 곳은 어디서든 대체될 수 있으며, 그 이후에도 프로그램은 작동할 것이다. 따라서 rectangle이 애플리케이션 내 어디에서 작동하든 square는 여전히 작동할 것이다; 누군가 rectangle를 square로 대체했기 때문에 애플리케이션은 실패해선 안 된다. 이것이 바로 Square가 Rectangle의 서브타입이라는 말의 의미다.
반면, 그 반대는 사실이 아니다. 애플리케이션이 square에 의존할 경우, 가령 크랩스 게임의 주사위 위에 얼굴을 형성할 경우, 임의의 rectangle은 작동하지 않을 것이다. 따라서 Rectangle은 Square의 서브타입이 아니다.
서브타입의 인스턴스를 자유롭게 대체하는 것은 다형성 만큼이나 수상하게 들리는데, 사실 그렇다. 다형성에서 프로그램은 변수 아래에 숨은 객체의 실제 타입을 알지 못한다-여느 서브타입도 그럴 것이다. 서브타입 관계는 무엇이 대체 가능한지를 명시하기 때문에 다형적 객체의 허용 가능한 범위도 명시할 것이다. 직설적으로 말하자면, 대체 가능성과 다형성은 동일한 개념에 해당하는 셈이다.
대체 가능성을 다른 방식으로 생각하려면 타입 S와 T의 변수 s와 t를 각각 생각해보자. T 인스턴스가 에상되는 곳 어디서든 S 인스턴스를 대체할 수 있는 자유도는 t := s라는 형태의 지정문이 옳음을 보여 준다. 이는 C++에서 살펴본 규칙과 동일하다 (168 페이지). Insect 변수는 butterfly를 참조할 수 있으나 그 반대는 불가하다.
다음과 같은 용어는 모두 동일하게 사용된다: AKO, 서브타입, 대체 가능, 일관적, 다형적. 모두들 미녀를 나타내며, 서브클래스의 개념과는 다르다. 이제부터는 이 차이를 구별할 필요가 있겠다.
논평: 서브셋에 대한 여담
타입은 세트를 생성한다. (또는 적어도 세트와 매우 닮은 무언가를 생성한다. 수학자들과 논리학자들은 여기서 "세트"라는 단어를 사용하는 것에 대해 불만을 표한다. 필자는 이러한 현상을 13 페이지에서 언급하였다.) 예를 들어, Butterfly 타입은 생성될 수 있는 모든 butterfly 객체들로 구성된 세트를 생성한다.
이러한 추론을 좀 더 따라서, 만일 S가 T의 서브타입이라면 S에 의해 생성된 세트는 T에 의해 생성된 세트의 서브세트이다. 그 이유는 서브타이핑이란 S가 T와 일관됨(AKO)을 의미한다는 주장에 동의했기 때문인데, 즉 T의 객체가 충족하는 것이 무엇이든 S의 객체도 충족함을 암시한다. 다시 말해, S의 객체들은 T의 객체들의 서브세트를 구성한다. 일상 언어에서 butterflies(나비)는 insects(곤충)의 서브세트를 구성하고 squares(사각형)는 rectangles(직사각형)의 서브세트를 구성한다.
서브타입은 자연스럽게 서브세트를 생성하지만, 그 반대로 슈퍼타입의 메서드와 동일하도록 정의된 메서드와 함께 슈퍼타입의 객체에 대한 임의 서브세트를 취하면 서브타입을 얻게 될 것이라는 주장을 하는 것은 무모할 것이다. 이는 슈퍼세트에서 적용되는 메서드가 서브세트에선 의미가 통하지 않을 수도 있기 때문이다. 예를 들어, rectangle이 길이를 두 배로 하고 너비를 절반으로 줄인 "squish(스쿼시)" 메서드를 갖고 있다고 가정해보자. Squares는 rectangle의 세트에서 타당한 서브세트를 형성함에도 불구하고 스쿼시될 수 없다. Squares는 rectangles가 이해하는 것과 동일한 메시지를 이해하지 못할 것이며, 스쿼시가 있을 때에 서브타입을 구성하지도 않을 것이다.
결과: 서브타입은 자연스럽게 서브세트를 생성하지만 서브세트는 굳이 자연스러운 서브세트를 정의할 필요가 없다.
논평: "일관성"이란 무슨 뜻인가?
직관적으로 우리는 서브타이핑이란 계층구조의 외부 일관성을 유지하는 것으로 이해한다. 하지만 이제까지 우리는 "일관성"에 대한 정확한 정의를 갖고 있지 않다. 예를 들어, 새는 날지만 펭귄은 날지 못한다. 펭귄이 새의 서브타입을 형성한다고 말할 수 있을까? 펭귄은 서브타입이 된다고 보장할 수 있을만큼 새와 충분히 일관성이 있는가? 펭귄의 fly 메서드가 새의 fly 메서드와 일관적이기 위해서는 어떤 조건을 따라야 하는가?
좀 더 엄격한 적용이 바람직하다. 우리는 일관성에 대한 객관적 시험을 원할 것이다. 불행히도 올바른 하나의 시험은 존재하지 않는다. 그 후보들은 일관성을 충족하기 위해 별로 많이 필요로 하지 않는 최소의 조건부터 일관성을 충족시키기가 극도로 힘든 강력하고 엄격한 조건까지 다양하다.
여기 네 가지 후보들이 있는데, 가장 약한 것부터 가장 엄격한 것까지 스펙트럼을 다룬다. 세기가 점점 세지는 순으로 아나키(anarchy), 준수(conformance), 행동 일관성(behavioral consistency), 경직성(rigidity)라고 부르겠다.
- Anarchy. S에는 T가 가진 모든 메시지 선택자를 최소한으로 갖고 있으며 어쩌면 그 이상을 가질 수도 있지만 메서드 bodies 자체가 하는 일에는 제한이 없다. 예를 들어, Bird에 fly 메서드가 있다면 Penguin 서브타입도 마찬가지로 갖고 있을 것이다. 하지만 Penguin>>fly는 No! 라고 응답하는 반면 Bird>>fly는 Yes!라고 응답할지 모른다. 좀 더 명백한 Penguin>>fly는 특수 오류 객체로 응답하거나 이 메시지를 애초에 보내지 말았어야 한다는 walkback을 생성할지도 모른다. 따라서 Bird>>fly의 취소 효과가 나타날 것이다. Penguin>>fly에 대한 가능성이 무제한인 아나키는 겨우 서브타이핑이라고 불릴 수 있다. 아직 스몰토크가 허용하는 것이 아닌 셈이다. 스몰토크는 서브타입 메서드가 하는 일에 어떤 제한도 가하지 않는다. 이것은 약한 일관성 형태로서 양심이 있는 사람이라면 서브타이핑의 정신을 이어받지 않는다고 말할 것이다. 스몰토크 프로그래머들이 서브타이핑이 아니라 서브클래싱을 한다고 말할 수 있다.
- Conformance(준수). 이제 스몰토크에서 벗어나보자; 준수는 변수에 타입이 있는 언어에 의해서만 검사할 수 있으며, 이미 알고 있듯이 스몰토크는 해당하지 않는다. 그럼에도 불구하고 객체 디자이너로서 준수에 대해 생각할 수는 있으며, 이를 이해한다면 자신의 디자인을 향상시킬 수 있을 것이다.
준수Conformance는 메서드의 리턴 값과 argument의 타입에 대한 일관성과 관련이 있다. 필자는 가장 널리 수용되는 정의를 논할 것이다. 먼저 "서브타입"이란 단어는 타입 자체를 포함한다는 규칙을 이용해보자. 그러면 타입은 항상 자신(itself)의 서브타입이 되고, 자신의 슈퍼타입이 되기도 한다.
S 는 그 메서드가 특정 규칙을 따를 경우 T 를 따를 것이다. 이러한 규칙에서 요지는 각 메서드가 T에서 상응하는 메서드보다 "더 많이 전달"하고 "덜 요구"하도록 강요하는 데에 있다. 더 많이 전달하고 덜 요구함으로써 S의 인스턴스는 더 도움이 될 것이다. 따라서 S의 인스턴스는 T의 인스턴스가 호출되는 곳 어디서든 대체될 수 있다.
개요는 이로 충분하니 이제 세부 사항을 알아보자. 우리는 메서드의 리턴 타입과 argument 타입을 생각할 필요가 있다. 먼저 리턴 타입을 살펴보자.
산란(laying egg)을 위한 메서드 Duck>>lay와 Bird>>lay를 고려해보자. Bird>>lay는 Egg의 인스턴스를 리턴하고 Duck>>lay는 DuckEgg의 인스턴스를 리턴한다. 따라서 Duck의 메서드는 Bird의 메서드보다 더 구체적인 객체 타입을 리턴한다-오리(ducks)는 산란 시 새(birds)보다 더 많은 양의 알을 낳는다. 우리는 Duck>>lay의 리턴 타입이 Bird>>lay의 리턴 타입을 따른다 고 말한다.
공식적 용어는 다음과 같다: m 메서드의 리턴 타입이 따르기 위해서는 S>>m이 리턴하는 객체의 타입이 T>>m이 리턴하는 타입의 서브타입이어야 한다. (리턴된 객체의 타입도 동일할 수 있음을 기억하라.) 이 도표의 오른쪽은 T>>m(Bird>>lay)와 S>>m(Duck>>lay)가 리턴할 수 있는 객체를 보여준다. (현재로선 왼쪽 그림은 무시한다.)
T>>m보다 S>>m이 리턴하는 객체가 적음을 주목하라-모든 알이 아니라 오리 알만 해당한다. 이것이 핵심이다. S>>m이 리턴하는 객체는 좀 더 구체적이다; T>>m이 리턴하는 객체의 서브타입이나 서브셋을 나타낸다.
리턴 타입의 준수에 대한 규칙이 있다-따분하게 간단하다는 것이다. 하지만 argument 타입에 대한 규칙에는 전환이 있다. 약간의 추가 표기가 필요하겠다: S>>m(P)는 메서드 m이 P 타입의 argument를 받아들임을 의미한다. 우리는 메서드 S>>m(P)와 메서드 T>>m(Q)의 arguments에서 P와 Q 타입이 따르는 의미가 무엇인지 정의할 것이다.
Piano>>play(Pianist)를 생각해보자: 다시 말해 pianos에 대한 play 메서드는 pianist라는 argument를 필요로 한다. Piano의 서브타입 ConcertGrand를 생각해보자. 누가 그랜드 피아노를 연주할 수 있을까? 다시 말하자면, ConcertGrand>>play(______)의 argument 타입에 어떤 조건을 부과해야 할까?
Argument 타입은 Pianist의 서브타입 Virtuoso여야 한다고 주장하고 싶을 것이다. 하지만 이러한 충동은 옳지 않으며, 준수에 관한 이 해석에서는 틀리다. 왜냐하면 우리는 규칙이 대체 가능성을 보장하길 원하는데, virtuoso가 그랜드 피아노를 연주할 수 있기를 요구할 경우 그랜드 피아노는 piano를 대체할 수 없기 때문이다! (예를 들어, 거실에서 피아니스트가 연주하고 있는 피아노를 생각해보자. 우리는 그랜드 피아노가 들어와 그것을 대체하고, 거실은 정상적으로 계속 돌아가길 원한다. 그렇지만 피아니스트 대신 거장(virtuoso)을 갑자기 요구하면 거실은 우리의 바람과 달리 정상적으로 돌아가지 않을 것이다. 대체가 작동하는 유일한 방법은 그랜드 피아노 역시 피아니스트가 연주하거나, 가령 Person과 같은 좀 더 일반적인 개인 타입이 피아니스트를 연주하는 것이다.)
역설적이게도, 대체 가능성을 보장하려면 ConcertGrand>>play(_____) 메서드가 Pianist의 슈퍼타입을 argument로 받아들여야 한다! 이러한 규칙은 음악성을 돋보이게 만들진 않지만 소프트웨어를 일관되게 만든다. (다중 메서드라고 알려진 디자인 기법은 이러한 문제에서 음악성을 유지한다. 이는 Piano와 Pianist를 동료 클래스(peer class)로 취급하고 두 개의 객체에서 "메서드"가 한 번에 작동하는 상상을 바탕으로 한다. 이러한 관점은 항상 하나의 객체를 선호하는 관습적인 객체 중심의 관점과는 다르다. 다중 메서드에 관한 논의는 170 페이지의 논평을 참고한다.)
따라서 argument 타입의 준수는 리턴 타입의 준수와는 정반대로 작용한다. 공식적 정의는 다음과 같을 것이다: 메서드 m의 argument 타입이 준수하려면 S>>m의 argument 타입은 T>>m에 해당하는 argument의 슈퍼타입이어야 한다. (다시 말하지만 타입은 같을 수 있다.) 위 그림의 왼쪽 절반을 보면 S>>m(ConcertGrand>>play)는 T>>m(Piano>>play)보다 더 가능한 argument를 수용한다. S>>m가 수용하는 arguments는 T>>m이 수용하는 arguments의 슈퍼세트나 서브타입을 나타낸다.
모두 준수를 바탕으로 서브타입의 정의부에 넣고 나면 우리는 S가 T의 서브타입이 되고, S는 T의 모든 메서드에 더해 어쩌면 더 많은 메서드를 가져야 하며, S와 T 내의 어떤 메서드 m이든 S>>m의 리턴 및 argument 타입은 T>>m의 리턴 및 argument 타입을 따라야 한다고 말할 수 있겠다. (해당 정의부는 재귀적임을 주목하라: S와 T 간 서브타입 관계는 arguments와 returns의 서브타입 관계와 관련해 설명된다.)
이 정의부를 표현하는 또 다른 방식은 C++ 공동체에서 유행했으나 수학 원리에서 젤 처음으로 사용한 용어로, 범주론을 사용한 방식이다. 리턴 타입은 공변성 규칙을 따른다-이는 S와 T로서 "같은" 방향으로 다양하다. 즉, S가 T의 서브타입일 경우, S>>m의 리턴은 T>>m의 리턴의 서브타입이어야 한다. 간략히 말해, S가 T의 메서드 모두를 포함해 그보다 더 많은 메서드를 갖고 있다면 S는 T의 서브타입이고, 공통된 여느 메서드든 리턴에서 공변성(covariance)과 arguments에서 반공변성(contravariance)을 따른다.
이러한 서브타이핑 규칙-준수-은 대체 가능성을 본질적 매력으로 하는 이론적 이상을 반영한다. 각 언어는 자체적 방식으로 서브타이핑 규칙을 정의하는데, 보통은 이러한 이상을 따르지 않는다. 스몰토크에서는 스몰토크 변수의 타입불가성(typelessness)때문에 준수가 상관이 없다. 컴파일러가 arguments와 리턴 타입을 검사하는 언어들 중에서 이러한 이상을 채택하는 언어는 거의 없다 (후에 소개할 표를 참고).
예를 들어, argument 타입 일관성에 대한 Eiffel의 규칙은 우리가 논한 바와 정반대다-반공변성 대신 공변성이 적용. Eiffel의 규칙은 격렬한 논쟁의 주제가 되어 왔다 ([Cook 1989] 참고). 이는 argument 타입의 공변성은 사실상 반공변성보다 더 유용하다는 이론적 원리를 가진다. 실용성(음악성)은 일관성(대체 가능성)에 대한 높은 욕구를 넘어서야 하므로 ConcertGrand>>play(Virtuoso)는 Piano>>play(Pianist)를 준수해야 한다. ConcertGrand>>play(Person)은 이론적으론 타당하고 Eiffel 의 argument가 될지 모르나 실제적 가치가 없다.
- 행위 일관성. 행위라 함은 메서드의 구문을 의미한다; 즉, 메서드의 이름이나 선택자 또는 서명에 그치지 않고 그들이 하는 일을 의미한다. 준수에 관한 정신과 동일하다: S가 T의 서브타입이 되기 위해선 S>>m은 T>>m보다 (행위적으로) 덜 요구하고 (행위적으로) 더 전달해야 한다.
메서드의 행위는 어떻게 명시할 수 있을까? 일반적인 기법은 선행조건(preconditions)과 후행조건(postconditions)을 사용하는 방법이다. 의미는 말 그대로다: 선행조건은 메서드가 실행 전에 무엇을 기대하는지로 구상되고 (예: 무엇을 필요로 하는지) 후행조건은 그것이 완료될 때 무엇을 보장하는지로 구성된다 (예: 무엇을 전달하는지). 따라서 적게 요구하고 더 많이 전달하는 것은 선행조건을 약화시키고 후행조건을 강화시킨다는 의미다.
자신의 메서드에 대한 선행조건과 후행조건을 고려하는 것은 훌륭한 교육이지만 스몰토크에서 할 수 있는 일이란 조건을 주석에 비형식적으로 기록하는 데에 그친다. 컴퓨터 과학자들은 형식적으로 선행조건과 후행조건을 명시하고 승인하는 실용적인 방법을 갖고 있지 않다. 따라서 메서드를 주석에 비형식적 선행조건과 후행조건으로 문서화하는 것을 부끄러워 할 필요가 없다. 상업용 객체 지향 언어들 중 Eiffel만이 이러한 종류의 일관성을 검사하기 위한 기본적인 지원까지 제공한다.
- 경직성. 일관성의 형태 중 가장 강력한 형태는 메서드의 alternate implementations를 범주적으로 금지하는 방법일 것이다. 객체 지향 개발자에게 이는 학술적이고 쓸모없는 개념이다. S는 T의 메서드에 완전히 새로운 메서드만 추가할 수 있을 것이다. Penguins는 bird 메서드에 원하는 수만큼의 메서드를 추가할 수 있지만 penguins는 고유의 fly 메서드를 가질 수 없게 된다. 대신 birds로부터 fly 메서드의 재사용만이 가능하다. Penguins는 다른 새처럼 날아야 하지만 이는 불만족스러운 조건이다. 어떤 객체 지향 언어도 이처럼 엄격하지는 않다. Alternate implementations를 금하면 받아들일 수 없는 객체 지향 언어가 생성될 것이다.
이제 이러한 이론적 개념을 실제 객체 지향 언어에서 일관성의 규칙으로 관련시켜보자. 아래 표는 여러 언어에서 이러한 규칙들을 설명한다. 이 규칙들은 다형성 또는 대체가 작동하는 시기를 결정하는 규칙으로 생각하라. 첫 열은 대체 가능성이 S와 T 간 명시적 구현 상속 관계를 필요로 하는지를 나타낸다; 즉, S를 T의 서브클래스로 선언을 의미한다. 나머지 열은 각 S>>m 메서드가 T>>m 메서드로부터 어떻게 벗어나는지를 설명한다.
S는 언제 T를 대체할 수 있는가? (다형성) | ||||
S는 T로부터 서브클래싱되어야 하는가? | S>>m이 T>>m과 일관되기 위해서는 | |||
리턴 타입 | Argument 타입 | 행위 조건 | ||
C++ | 그렇다 | 공변적 | 일치해야 한다 | 해당사항 없음 |
Eiffel | 그렇다 | 공변적 | 공변적 | 선행조건을 약하게 하거나 후행조건을 강하게 할 수도 있다. |
Emerald | 아니다 | 공변적 | 반공변적 | 해당사항 없음 |
Java | 아니다 | 공변적 | 일치해야 한다 | 해당사항 없음 |
Modula-3 | 그렇다 | 일치해야 한다 | 일치해야 한다 | 예외를 적게 발생시킬 수 있다. |
POOL-I | 아니다 | 공변적 | 공변적 | 더 많은 "프로퍼티"를 가질 수도 있다. |
Smalltalk | 아니다 | 제한 없음 | 제한 없음 | 해당사항 없음 |
이론적 이상 | 아니다 | 공변적 | 반공변적 | 선행조건을 약하게 하거나 후행조건을 강하게 할 수도 있다. |
C++에서 다형성은 본래 보수적이었다: arguments와 리턴에 선언된 타입에 일어난 변경은 다형성을 무시했다. 하지만 ANSI C++ 위원회는 1993년 조치를 취하여 표에 나타난 바와 같이 리턴 타입에 다형성을 허용하였다.
Emerald와 POOL-I 연구 언어만 이상적 준수(conformance)를 지원한다는 사실을 주목하라-리턴 타입에서 공변성과 argument 타입에서 반공변성. 표 안의 언어에 대한 타입 시스템에 관한 추가 정보는 [Stroustrup 1991; Meyer 1992; Black et al. 1986; Sun 1995; Cardelli et al. 1992; America 1991; Goldberg and Robson 1983]을 참고한다.
일관성과 스몰토크
준수와 같은 주제에 대한 논의가 애초에 변수가 타입을 가지지 않는 스몰토크와는 과연 무슨 상관이 있을까? 디자이너가 스몰토크 언어에서 이러한 개념을 표현하지 못한다고 해서 생각까지 못하는 것은 아니다. 일관성 개념을 의식하지 못하는 디자이너는 사실 특이한 디자이너일 것이다. 그리고 이것이 바로 이번 장의 핵심에 해당한다: 당신은 스몰토크 개발자로서 일관성에 대한 자신의 이상적인 사고와, 자신의 소프트웨어에 이러한 사고를 표현할 수단이 없다는 사실에 조화를 이루어야 한다고 경고하는 것이다. 당신에겐 하나의 상속 매커니즘이 있으며, 가장 쉬운 방식은 서브클래싱에 사용하는 것이다.
초보자들은 AKO에서 (서브타이핑) 대부분 동기를 부여 받지만 점점 클래스와 자신의 작업에 익숙해지면서 그들이 발견한-그들이 서브클래싱하는-코드의 재사용을 위해 상속을 하기 시작한다. 결국 전문 스몰토크 프로그래머들은 서브클래싱에 상속을 자주 사용하게 된다. 이유는 간단하다. 다른 객체 지향 언어에 비해 스몰토크에서는 상속 시 모든 것을-모든 인스턴스 변수와 모든 메서드-상속하기 때문이다. 원하든 원하지 않든 모든 내부로의 접근성을 얻는다. 이것은 생산자의 모래상자인 셈이다[1].
모든 내부를 상속해야 하는 제약을 받는 스몰토크 개발자들은 타입 계층구조를 정의할 방법이 없다. 제약은 매우 단호한 듯 보이지만 보기만큼 심하진 않다. 클래스와 타입 계층구조는 종종 같거나 거의 동일한데, 이것이 바로 많은 스몰토크 개발자들이 차이를 알지 못하면서도 살아남는 이유다. 다시 강조하건대, 항상 그런 것은 아니며 종종 그런 상황이 있다. 스몰토크의 클래스 계층구조를 가리고 대신 서브타입 관계를 기대한다면 우리는 무엇을 보게 될까?
William Cook은 Smalltalk-80에서 컬렉션 클래스에 이 실험을 진행했는데....완전히 다른 계층구조를 발견한 것이 아닌가! [Cook 1991] 다음에 소개할 연습에서는 그의 실험을 따라해 볼 것이다.
연습: 스몰토크의 컨테이너 "타입"
다음과 같은 컨테이너 클래스를 생각해보자: Array, Bag, Collection, Dictionary, Set, String. (bag는 set와 같은데, bag 내에서는 요소가 한 번 이상 발생한다는 점만 다르다; set에는 요소를 두 번 추가할 수 없다.) 우리의 목표는 적절한 타입(AKO) 계층구조에 이들을 배열하는 것이다. 이러한 작업을 위해선 먼저 이 클래스들의 소비자에 적절한 public 선택자들을 먼저 살펴봐야 한다. 대표 리스트가 있다-size, at:, at:put:, includes:, <. indexOf:, remove;, removeKey:, add:withOccurrences:. 이는 부자연스러운 메서드의 서브세트지만-완전한 평가를 위해선 모든 public 메서드를 수반할 것이다-이 연습을 쉽게 다룰 수 있도록 유지하고자 한다.
일관성에 대한 정의도 정해야 하겠다. 간단한 정의를 사용해보자: S가 T의 모든 선택자를 가진다면 S는 T의 서브타입이고, 이 둘은 모두 작동한다. 우리는 오류를 전송하는 메서드의 수는 세고 싶지 않다고 말하고 있기 때문에 무질서(anarchic) 서브타이핑 규칙(202 페이지)보다는 약간 더 까다롭다. 메서드에서 의도적 오류는 개발자가 메서드를 무효화하고 싶어함을 알려준다. 우리는 이러한 조건을 취소(cancellation)라 불렀다. 또한 일부 메서드들은 명시적으로 취소되지 않음에도 불구하고 올바로 작동하지 않는다. 많이 실패하는 메서드의 한 가지 예가 바로 at:인데, Object에서 정의되지만 대부분 클래스에서 실패한다.
각 클래스에 유효한 선택자를 표로 작성하라. 브라우저를 사용해야 하지만 필자는 size 메서드가 Array 클래스에 의해 지원받는지를 검사할 Array new size와 같은 실험적 메시지를 실행하여 자신의 브라우징을 보충할 것을 권한다. 충분히 분석을 실행하는 데에는 어느 정도 시간이 소요될 것이다. 어떤 클래스가 어떤 선택자를 지원하는지 결정한 후에 클래스를 실행 가능한 타입 계층구조로 배열하라.
해결책과 논의
이러한 컬렉션 클래스를 훑어본 결과가 여기 있다. 당신의 결과도 동일할 것이다. 사실 이러한 클래스들은 모두 표준 스몰토크 클래스에 해당하기 때문에 당신이 사용하는 스몰토크 dialect가 무엇이든 동일한 결과에 도달해야 한다. (더하기(+)는 메서드가 클래스에 유효함을 의미한다.)
size | at: | at:put: | includes: | < | indexOf: | remove: | removeKey: | add:withOccurrences: | |
Array | + | + | + | + | + | ||||
Bag | + | + | + | + | |||||
Collection | + | + | |||||||
Dictionary | + | + | + | + | + | ||||
Set | + | + | + | ||||||
String | + | + | + | + | + | + |
첫 열을 분석하자면, IBM Smalltalk에서 Array, Collection, String이 Object로부터 size 메서드를 성공적으로 상속받음을 발견할 수 있다. 하지만 Bag, Dictionary, Set는 그렇지 않았다. 대신 각각이 고유의 size 메서드로 Object>>size를 오버라이드한다. 그럼에도 불구하고 어떤 수단을 이용하든 6개 클래스는 모두 작동하는 size 메서드를 갖고 있었다. 따라서 각 클래스의 첫 열에 +가 표시되어 있다. (다른 스몰토크 dialect에서 size 메서드는 클래스 계층구조 내 어딘가에서 시작되며, 다른 근접(immediate) 클래스와 취소 및 재구현도 그를 따라 발생한다. 아무렴 상관없다. 여전히 6개의 클래스 모두 작동하는 size 메서드를 갖고 있음을 발견할 것이기 때문이다.)
두 번째와 세 번째 열에서 필자는 Array가 Object로부터 at:과 at:put:을 성공적으로 상속하고, Dictionary와 String은 고유의 오버라이드를 제공함을 알아냈다. 그 외 클래스들-Bag, Collection, Set-은 Object로부터 at:과 at:put:을 상속받긴 하지만 메서드들이 walkback을 생성한다. 따라서 Array, Dictionary와 String만 2, 3열에 +가 표기되어 있다.
이렇게 조심스럽게 분석을 계속해 나가면서 표의 나머지 부분을 채워나갔다. 다시 말하지만, 자신의 경로에서 세부내용은 다를 수 있지만 여느 스몰토크 표준 dialect에서든 동일한 결과에 도달해야 한다. 표를 바탕으로 아래 다이어그램과 같이 타입 계층구조를 제안할 수 있겠다.
각 타입에는 타입과 타입의 서브타입에 관계가 있는 메시지 선택자들의 이름이 붙어 있다. 만들어낸 타입인 Locatable을 주목하라. 이는 우리가 Array와 Dictionary, 그리고 그들의 서브타입을 제공하는 선택자 at:과 at:put:을 팩토링할 수 있는 편리한 타입이다.
연습: 스몰토크의 컨테이너 "클래스"
소비자는 위의 타입 계층구조를 고마워하는데, 그 이유는 그들이 구매하고자 하는 클래스를 위치시키기가 쉽기 때문이다; 이는 직관적 AKO 계층구조다. 안타깝게도 스몰토크에서 보는 계층구조는 다르다. 이는 생산자의 계층구조-야수-로, 계층구조를 빌드한 사람들에게 최적화된 것이지 직관적 위치에서 클래스를 찾길 바라는 사람들을 위한 것이 아니다.
❏ 스몰토크에서 실제 클래스 계층구조를 그리기 위해 브라우저를 사용하라.
해결책과 논의
우리가 브라우저에서 직접 도출한 IBM Smalltalk 클래스 계층구조는 아래와 같다:
AKO 계층구조를 기대하는 순진한 소비자는 이 그림을 보고 충격을 받을 것이다. Sets와 Bags가 상관관계가 없고, arrays와 strings는 직관적 AKO 관계를 손실하였다. 이유가 뭘까?
소비자에게는 나쁜 소식인데, 이러한 클래스들은 생산자가 자신에게 이익이 되도록 빌드한 상태다. 소비자는 이를 모르지만 bags는 그 내부에서 dictionary와 같은 객체를 이용해 빌드되었기 때문에 sets로부터 상속할 필요가 없다. Strings는 유일하고 압축된 방식으로 저장되어 (195 페이지에 그림을 기억하라) arrays로부터 상속하지 아니한다. 나머지도 마찬가지다.
그럼에도 불구하고 이렇게 소비자가 알고 싶어 하지 않는 내부 디자인 결정은 클래스 계층구조의 구조에 영향을 미친다. 클래스 계층구조는 여기서 보는 바와 같이 AKO 계층구조가 아니라 하더라도 소비자에 대한 AKO 기대치를 설정한다. 그리고 이것은 이번 장의 주제에서 발생하는 운이 나쁜 부산물에 해당한다: 스몰토크에는 하나의 계층구조밖에 없으며, 이는 두 개의 양립이 불가한 마스터(master), 즉 서브클래싱과 서브타이핑을 동시에 제공할 수 없다. 스몰토크의 브라우저를 통해 목격한 컬렉션 계층구조는 서브클래스 계층구조이지 당신이 보고 싶어 하는 서브타입 계층구조가 아니다. 타입 계층구조는 개념적으로 존재하긴 하지만 실용적 목적을 위해 눈에 보이지 않는다. 이를 찾아내기 위해 엄청난 작업을 해야 했다.
그건 그렇고, ParcPlace-Digitalk Smalltalk 브라우저에서 도출한 클래스 계층구조도 도움이 되지 않는다:
이 계층구조는 자연스러운 타입 관계를 포착하지 못하는데, 그 이유는 더 당혹스럽다-Dictionary가 Set로부터 상속 받기 때문이다. 생산자는 구현을 이유로 이 디자인을 선택했다: set 내에서 캡슐화된 것은 array로, dictionaries가 고유의 private 목적을 위해 상속한 것이다.
스몰토크에서 나타나는 타입 대 클래스의 딜레마는 가장 먼저 [LaLonde et al. 1986]에서 나타난다. 스몰토크 컬렉션 클래스에 대한 철저한 분석은 [Cook 1991]를 참조한다.
요약
객체에는 외부와 내부가 있다. 사각형이 특별한 유형의 직사각형으로 알려져 있다는 사실을 생각해보면 우리는 관습적 행위-그것의 외부-를 생각하는 것이다. 사각형에 하나의 side 인스턴스 변수만 있고 직사각형에는 두 개가 있다고 생각하면, 사용자에게 스스로를 어떻게 표현하는지보다는 그것이 어떻게 내부를 구성하는지를 생각할 것이다. 슬픈 사실은 오늘날 대부분 객체 지향 언어는 우리의 마음과 똑같은 방식으로 구분하지 않는다는 사실이다; 서브클래싱으로 서브타이핑을 혼란스럽게 만들어 놓는다.
다형성을 결정짓는 것은 서브클래싱이 아니라 서브타이핑이다. 우리는 타입이 없는 변수를 가진 스몰토크를 매우 너그러운 타입 시스템이라고 비난한다 (206 페이지에 표를 다시 참고하라). 스몰토크는 타입 계층구조의 형성에 어떤 규칙도 강요하지 않는 반면 다른 객체 지향 언어들은 타입 관계에서 일관성을 검사하느라 땀을 뺀다.
그에 대한 보상으로, 표의 첫째 열에 따라 스몰토크는 우리가 암시적(implicit) 다형성이라 (169 페이지) 부르는 것을 지원한다. 이 다형성은 클래스들 간에 선언된 관계에 의존하지 않는다. 두 클래스가 동일한 메시지를 지원한다면, 클래스 계층구조에서 서로 무관한 부분이라 하더라도 그들은 일관성이 있는 것이다. Puddle과 CarBattery 둘 다 jump와 drain을 지원할 경우 스몰토크는 그들을 대체 가능한 타입으로 간주하므로 서로 다형적으로 행동할 수도 있다. 스몰토크 컬렉션에 대한 분석을 되돌아보면, Bag은 Set의 서브타입이므로 둘이 상속으로 관계가 없다 하더라도 bag는 set를 다형적으로 대체할 수 있을지 모른다.
논평: 스몰토크 표준화하기
위 연습에서 컬렉션 클래스에 대한 분석은 흥미로운 질문을 제기한다: ParcPlace-Digitalk와 IBM dialects의 양립 불가한(incompatible) 컬렉션 계층구조는 스몰토크의 표준화에 대한 장애물인가? 이 질문에 대한 ANSI 위원회의 응답은 "아니오"였다. 문제에 대한 그들의 최신 접근법은 스몰토크 클래스 계층구조보다는 타입 계층구조를 표준화하는 것이다. 이는 서브클래스 기반의 상속에 대한 스몰토크의 전형적인 사고방식으로부터의 혁신적인 시도인 셈이다.
ANSI 접근법이 컬렉션 클래스와 어떻게 들어맞는지를 주목하라: 필자는 연습에 제시한 해결책을 통해, dialects에 대한 타입 분석의 결과가 동일하다고 언급한 바 있다. 즉, ParcPlace-Digitalk와 IBM의 컬렉션 클래스 계층구조는 서로 불일치할지도 모르지만 컬렉션 타입 계층구조는 일치한다는 말이다. 그리고 소비자에게 중요한 것은 타입 계층구조다. 타입 계층구조는 객체의 행위에 대한 직관적 이해를 나타내며, 다른 객체들을 대신해 다형적으로 사용할 수 있는 객체들을 통제한다.
어떤 표준이든 주요 매력은 이식 가능한 코드의 가능성이다. 스몰토크 코드를 ANSI 표준 dialect에서 다른 dialect로 이식할 수 있도록 만들고 싶다면, 코드를 작성 시 한 가지 지침을 따라야 한다: 표준 클래스로부터 구매만 해야 할 뿐 상속은 하면 안 된다. 구매를 통해 우리는 클래스의 표준화된 인터페이스만 사용한다. 반면 표준 클래스로부터의 상속이라 하더라도 상속은 꼭 문제를 야기하기 마련인데, 상속은 우리의 서브클래스를 클래스의 표준 외부(미녀) 대신 비표준 내부(야수)로 연결하기 때문이다.
애플리케이션의 일부만 이식 가능할 가능성이 높다는 사실도 유념하기 바란다. ANSI 표준은 기반 클래스-containers, magnitudes, streams 등-에 초점을 둘 것이다. 이들은 모든 스몰토크 애플리케이션을 위한 기본 빌딩 블록이지만, 실제로 작동하는 애플리케이션은 표준화될 가능성이 적은 수많은 다른 클래스를 사용한다. 하지만 그러한 클래스의 사용에는 지속성(persistence)과 데이터베이스 클래스, 통신 클래스 등이 따라온다. 우리는 표준 dialect들 간 모델 코드에서 문제가 없이 이식이 가능하길 바라는 것이 최선이다; 애플리케이션의 나머지 부분은 작업을 필요로 할 것이다.
Notes
- ↑ 기술적 여담: 반면 C++는 상속에 대한 통제력을 더 제공한다. C++에서 private members와 private inheritance는 어떤 서브클래스가 접근할 수 있는지와 서브클래스의 어떤 소비자가 접근할 수 있는지를 제한한다. 이러한 특징이 바로 고유의 구현부를 위해 클래스가 상속하는 것을 소비자에게 표시되는 모양을 위한 상속으로부터 구분한다-서브타이핑을 서브클래싱과 구분하는 데에 있어 중요한 단계. Java는 이보다 더하다. 이는 두 가지 개념을 지원한다-일반적인 클래스에 인터페이스까지. 인터페이스는 메서드명의 세트를 명시한다. 프로그래머는 구분된 클래스 및 상속 계층구조를 개발하여 특정 클래스를 마음대로 특정 인터페이스와 연관시킬 수 있다. 그리고 짐작했듯이 Java에서 다형성(대체 가능성)은 객체의 클래스가 아니라 인터페이스에 따라 좌우된다.