오브젝트 독서회 12회차 - 13장 서브클래싱과 서브타이핑

이번에는 우수참여자이신 @freebear 님께서 독감으로 함께하시지 못하게 되셨습니다.

13장까지 온 지금, 인터페이스에 대한, 메시지에 대한 이해가 점점 높아져가고 있습니다.

다음달까지는 완주할 수 있으면 좋겠습니다.


P435

상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이어야 한다.

@은딩

타입 사이의 관계를 고려하지 않은 채 단순히 코드를 재사용하기 위해 상속을 사용해서는 안된다.

@은딩

P436

**객체기반 프로그래밍(Object-Based Programming)**이란 상태와 행동을 캡슐화한 객체를 조합해서 프로그램을 구성하는 방식을 가리킨다. 이 정의에 따르면 **객체지향 프로그래밍(Object-Oriented Programming)**역시 객체기반 프로그래밍의 한 종류다. 객체지향 프로그래밍은 객체기반 프로그래밍과 마찬가지로 객체들을 조합해서 애플리케이션을 개발하지만 상속다형성을 지원한다는 점에서 객체기반 프로그래밍과 차별화된다.

@vincent @은딩

P437

지금까지의 설명을 통해 타입이 심볼, 내연, 외연의 세 가지 요소로 구성된다는 사실을 알 수 있다[Martin98, Larman04].

  • 심볼(symbol) 이란 타입에 이름을 붙인 것이다. 앞에서 '프로그래밍 언어’가 타입의 심볼에 해당된다.
  • 내연(intension) 이란 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동을 가리킨다. '프로그래밍 언어’의 정의인 '컴퓨터에게 특정한 작업을 지시하기 위한 어휘와 문법적 규칙의 집합’이 바로 내연에 속한다. 일반적으로 타입에 속하는 객체들이 공유하는 속성과 행동의 집합이 내연을 구성한다.
  • 외연(extension) 이란 타입에 속하는 객체들의 집합이다. ‘프로그래밍 언어’ 타입의 경우 자바, 루비, 자바스크립트, C가 속한 집단이 외연을 구성한다.

@vincent

P438

정리하면 타입은 적용 가능한 오퍼레이션의 종류와 의미를 정의함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용된다.

@vincent

객체지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미한다. 따라서 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.

@vincent @은딩

객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다.

@은딩

객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.

@은딩

P439

객체를 바라볼 때는 항상 객체가 외부에 제공하는 행동에 초점을 맞춰야 한다.

@은딩

P441

일반화와 특수화를 다음과 같이 정의할 수 있다[Martin98].
일반화는 다른 타입을 완전히 포함하거나 내포하는 타입을 식별하는 행위 또는 그 행위의 결과를 가리킨다.
특수화는 다른 타입 안에 전체적으로 포함되거나 완전히 내포되는 타입을 식별하는 행위 또는 그 행위의 결과를 가리킨다.

@vincent @은딩

P443

서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.

@은딩

마틴 오더스키는 다음과 같은 질문을 해보고 두 질문에 모두 '예’라고 답할 수 있는 경우에만 상속을 사용하라고 조언한다[Odersky11].
상속 관계가 is-a 관계를 모델링하는가?
미것은 애플리케이션을 구성하는 어휘에 대한 우리의 관점에 기반한다. 일ㅇ반적으로 "[자식 클래스]는 [부모 클래스]다"라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주할 수 있다.
클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?
상속 계층을 사용하는 클라이언트의 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 한다. 이를 자식 클래스와 부모 클래스 사이의 행동 호환성 이라고 부른다.

@vincent

P445

이 예는 어휘적인 정의가 아니라 기대되는 행동에 따라 타입 계층을 구성해야 한다는 사실을 잘 보여준다. 어휘적으로 펭귄은 새지만 만약 새의 정의에 날 수 있다는 행동이 포함된다면 펭귄은 새의 서브타입이 될 수 없다. 만약 새의 정의에 날 수 있다는 행동이 포함되지 않는다면 펭귄은 새의 서브타입이 될 수 있다. 이 경우에는 어휘적인 관점과 행동 관점이 일치하게 된다.

@vincent

따라서 타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다.

@은딩

따라서 어떤 두 대상을 언어적으로 is-a라고 표현할 수 있더라도 일단은 상속을 사용할 예비 후보 정도로만 생각하라. 너무 성급하게 상속을 적용하려고 서두르지 마라. 여러분의 애플리케이션 안에서 두 가지 후보 개념이 어떤 방식으로 사용되고 협력하는지 살펴본 후에 상속의 적용 여부를 결정해도 늦지 않다.

@vincent

타입의 이름 사이에 개념적으로 어떤 연관성이 있다고 하더라도 행동에 연관성이 없다면 is-a 관계를 사용하지 말아야 한다.

@vincent

단순히 동일한 메서드를 구현하고 있으면 행동이 호환되는 것일까?
여기서 중요한 것은 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점 이라는 것이다.

@vincent @은딩

클라이언트가 두 타입이 동일하게 행동하지 않을 것이라고 기대한다면 두 타입을 타입 계층으로 묶어서는 안된다.

@은딩

P446

하지만 대부분의 사람들은 "펭귄이 새다"라는 말에 현혹당한 채 상속 계층을 유지할 수 있는 해결 방법을 찾으려 할 것이다. 상속 관계를 유지하면서 문제를 해결하기 위해 시도해 볼 수 있는 세 가지 방법이 있다.

@vincent

첫 번째 방법은 Penguin의 fly 메서드를 오버라이딩해서 내부 구현을 비워두는 것이다.

@vincent

두 번째 방법은 Penguin의 fly 메서드를 오버라이딩한 후 예외를 던지게 하는 것이다.

@vincent

세 번째 방법은 flyBird 메서드를 수정해서 인자로 전달된 bird의 타입이 Penguin이 아닐 경우에만 fly 메시지를 전송하도록 하는 것이다.

@vincent

P447

일반적으로 instanceof처럼 개체의 타입을 확인하는 코드는 새로운 타입을 추가할 때마다 코드 수정을 요구하기 때문에 개방-폐쇄 원칙을 위반한다.

@은딩

P449

인터페이스는 클라이언트가 기대하는 바에 따라 분리돼야 한다는 것을 기억하라. 하나의 클라이언트가 오직 fly 메시지만 전송하기를 원한다면 이 클라이언트에게는 fly 메시지만 보여야 한다. 다른 클라이언트가 오직 walk 메시지만 전송하기를 원한다면 이 클라이언트에게는 walk 메시지만 보여야 한다. 따라서 가장 좋은 방법은 fly 오퍼레잇연을 가진 Flyer 인터페이스와 walk 오퍼레이션을 가진 Walker 인터페잇으로 분리하는 것이다.

@vincent @은딩

P450

클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있게 된다. 대부분의 경우 인터페이스는 클라이언트의 요구가 바뀜에 따라 변경된다. 클라이언트에 따라 인터페이스를 분리하면 각 클라이언트의 요구가 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있게 된다.

@은딩

이처럼 인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙(Interfacer Segregation Principle, ISP) 이라고 부른다.

@vincent @은딩

P451

설계가 꼭 현실 세계를 반영할 필요는 없다는 것이다. 중요한 것은 설계가 반영할 도메인의 요구사항이고 그 안에서 클라이언트가 객체에게 요구하는 행동이다.

@vincent

요점은 자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중하라는 것이다. 클래스의 이름 사이에 어떤 연관성이 있다는 사실은 아무런 의미도 없다. 두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안 된다.

@vincent @은딩

사람들은 상속을 사용하는 두 가지 목적에 특별한 이름을 붙였는데 서브클래싱서브타이핑 이 그것이다.

@vincent

서브클래싱(subclassing): 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 가리킨다.

@vincent

서브타이핑(subtyping): 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다.

@vincent

P452

즉, 추상클래스를 상속한다는 것은 단순한 코드의 재사용을 위한 상속이 아니라 추상 클래스가 정의하고 있는 인터페이스를 상속하겠다는 의미인 것이다[GOF 1994].

@은딩

슈퍼타입 인스턴스를 요구하는 모든 곳에서 서브타입의 인스턴스를 대신 사용하기 위해 만족해야 하는 최소한의 조건은 서브타입의 퍼블릭 인터페이스가 슈퍼타입에서 정의한 퍼블릭 인터페이스와 동일하거나 더 많은 오퍼레이션을 포함해야 한다는 것이다. 따라서 개념적으로 서브타입이 슈퍼타입의 퍼블릭 인터페이스를 상속받는 것처럼 보이게 된다. 이것이 서브타이핑을 인터페이스 상속이라고 부르는 이유다.

@vincent

P453

즉, 어떤 타입이 다른 타입의 서브타입이 되기 위해서는 행동호환성(behavioral substitution)[Riel96, Jacobson92, Tavalsaari96]을 만족시켜야 한다.

@은딩

리스코프 치환 원칙은 앞에서 논의한 행동 호환성을 설계 원칙으로 정리한 것이다. 리스코프 치환 원칙에 따르면 자식 클래스가 부모 클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑이라고 불러야 한다.

@vincent @은딩

P458

리스코프 치환 원칙은 "클라이언트와 격리한 채로 본 모델은 의미 있게 검증하는 것이 불가능하다[Martin02]"는 아주 중요한 결론을 이끈다. 어떤 모델의 유효성은 클라이언트의 관점에서만 검증하다는 것이다.

@은딩

대체 가능성을 결정하는 것은 클라이언트다.

@은딩

is-a는 클라이언트 관점에서 is-a일 때만 참이다. 정사각형은 직사각형인가? 클라이언트가 이 둘을 동일하게 취급할 수 있을 때만 그렇다. 펭귄은 새인가? 클라이언트가 이 둘을 동일하게 취급할 수 있을 때만 그렇다.

@vincent

P459

결론적으로 상속이 서브타이핑을 위해 사용될 경우에만 is-a 관계다. 서브 클래싱을 구현하기 위해 상속을 사용했다면 is-a 관계라고 말할 수 없다.

@은딩

리스코프 치환 원칙은 클라이언트가 어떤 자식클래스와도 협력할 수 있는 상속 구조를 구현할 수 있는 가이드라인을 제공한다.

@은딩

다시 말해서 클라이언트의 입장에서 퍼블릭 인터페이스의 행동 방식이 변경되지 않는다면 클라이언트의 코드를 변경하지 않고도 새로운 자식 클래스와 협력할 수 있게 된다는 것이다.

@은딩

P460

자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어진다. 따라서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다. 일반적으로 리스코프 치환 원칙 위반은 잠재적인 개팡-폐쇄 원칙 위반이다[Martin02].

@vincent @은딩

P461

상속이 아닌 다른 방법을 이용하더라도 클라이언트의 관점에서 서로 다른 구성요소를 동일하게 다뤄야 한다면 서브타이핑 관계의 제약을 고려해서 리스코프 치환 원칙을 준수해야 한다.

@은딩

클라이언트와 서버 사이의 협력을 의무(obigation)와 이익(benefit)으로 구성된 계약의 관점에서 표현하는 것을 **계약에 의한 설계(Desgin By Contract, DBC)**라고 부른다.

@은딩

서브 타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 '계약’을 준수해야 한다.

@은딩

P462

코드 재사용을 위해 상속을 사용했다면, 그리고 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 없다면 서브타입이라고 할 수 없다.

@은딩

4개의 좋아요

12회차 때는 제가 독감에 걸려 불참을 하였습니다.
따라서 따로 독서 후 인용구를 댓글로 등록합니다.

P435

결론부터 말하자면 동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다. 상속의 가치는 이러한 타입 계층을 구현할 수 있는 쉽고 편안한 방법을 제공한다는 데 있다. 타입 사이의 관계를 고려하지 않은 채 단순히 코드를 재사용하기 위해 상속을 사용해서는 안 된다.

=> 저번 챕터 12의 내용을 한 번 더 강조하고 시작하고 있습니다.

P436

객체지향 프로그래밍은 객체기반 프로그래밍과 마찬가지로 객체들을 조합해서 애플리케이션을 개발하지만 상속과 다형성을 지원한다는 점에서 객체기반 프로그래밍과 차별화된다.

P438

프로그래밍 언어의 관점에서 타입은 호출 가능한 오퍼레이션의 집합을 정의한다. 객체지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미한다. 따라서 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.

바로 퍼블릭 인터페이스가 그것이다.

객체지향 프로그래밍 관점에서 타입을 다음과 같이 정의할 수 있다.

[객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.]

P439

객체를 바라볼 때는 항상 객체가 외부에 제공하는 행동에 초점을 맞춰야 한다. 객체의 타입을 결정하는 것은 내부의 속성이 아니라 객체가 외부에 제공하는 행동이라는 사실을 기억하라.

P442

퍼블릭 인터페이스의 관점에서 슈퍼타입과 서브타입을 다음과 같이 정의할 수 있다.

[슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것이다.]

[서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한 것이다.]

P445

어휘적으로 펭귄은 새지만 만약 새의 정의에 날 수 있다는 행동이 포함된다면 펭귄은 새의 서브타입이 될 수 없다. 만약 새의 정의에 날 수 있다는 행동이 포함되지 않는다면 펭귄은 새의 서브타입이 될 수 있다. 이 경우에는 어휘적인 관점과 행동 관점이 일치하게 된다.

따라서 타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다.

결론은 두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 한다는 것이다.

여기서 중요한 것은 행동의 호환 여부를 판단하는 기준은 클라이언트 관점이라는 것이다.

P451

요점은 자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중하라는 것이다. 클래스의 이름 사이에 어떤 연관성이 있다는 사실은 아무런 의미도 없다. 두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안 된다.

P453

자식 클래스와 부모 클래스 사이의 행동 호환성은 부모 클래스에 대한 자식 클래스의 대체 가능성을 포함한다.

행동 호환성과 대체 가능성은 올바른 상속 관계를 구축하기 위해 따라야 할 지침이라고 할 수 있다.

P458

리스코프 치환 원칙은 상속 관계에 있는 두 클래스 사이의 관계를 클라이언트와 떨어트려 놓고 판단하지 말라고 속삭인다. 상속 관계는 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 있을 때만 올바르다.

P459

행동을 고려하지 않은 두 타입의 이름이 단순히 is-a로 연결 가능하다고 해서 상속 관계로 연결하지 마라. 이름이 아니라 행동이 먼저다.

지금까지 살펴 본 것처럼 리스코프 치환 원칙은 클라이언트가 어떤 자식 클래스와도 안정적으로 협력할 수 있는 상속 구조를 구현할 수 있는 가이드라인을 제공한다.

리스코프 치환 원칙을 따르는 설계는 유연할뿐만 아니라 확장성이 높다.

P461

서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 ‘계약’을 준수해야 한다.

P465

계약의 관점에서 상속이 초래하는 가장 큰 문제는 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 수 있다는 것이다.

P466

서브타입에 더 강력한 사전조건을 정의할 수 없다.

P467

서브타입에 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있다.

서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있다.

P468

서브 타입에 더 약한 사후조건을 정의할 수 없다.

2개의 좋아요