13장 서브클래싱과 서브타이핑 #
01. 타입 #
- 객체지향 프로그래밍에서 타입의 의미를 이해하려면 프로그래밍 언어 관점에서의 타입과 개념 관점에서의 타입을 함께 살펴볼 필요가 있다.
개념 관점의 타입 #
- 개념 관점의 타입이란 우리가 인지하는 세상의 사물의 종류를 의미한다.
- 우리가 인식하는 객체들에 적용하는 개념이나 아이디어를 가리켜 타입이라고 부른다.
- 어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스라고 한다. 일반적으로 인스턴스를 객체로 부름.
프로그래밍 언어관점의 타입 #
- 프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용한다.
객체지향 프로그래밍 관점의 타입 #
- 객체지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미한다.
- 따라서 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.
- 객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다.
02. 타입 계층 #
- 타입 계층을 구성하는 두 타입 간의 관계에서 더 일반적인 타입을 슈퍼타입이라고 부르고, 더 특수한 타입을 서브타입이라고 부른다.
- 객체의 의미를 정의하는 내연 관점에서 일반화란 어떤 타입의 정의를 좀 더 보편적이고 추상적으로 만드는 과정을 의미한다.
- 반대로 특수화란 어떤 타입의 정의를 더 구체적이고 문맥 종속적으로 만드는 과정을 의미한다.
- 객체지향의 퍼블릭 인터페이스의 관점에서 슈퍼타입과 서브타입을 다음과 같이 정의할 수 있다.
- 슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것이다.
- 서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 구체적이고 좁은 의미로 정의한 것이다.
03. 서브 클래싱과 서브 타이핑 #
언제 상속을 사용해야 하는가? #
- 상속의 올바른 용도는 타입 계층을 구현하는 것이다.
- 다음 질문을 모두 충족할 때 사용하라
- 상속 관계가 is-a 관계를 모델링하는가?
- 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무관한가?(행동 호환성)
is-a 관계 #
- 펭귄은 새다.
- 펭귄은 날 수 없는 새다.
- Bird 클래스에 fly 메서드가 있다면 펭귄은 Bird의 서브타입이 될 수 없다.
- 어휘적인 정의가 아니라 기대되는 행동에 따라 타입 계층을 구성해야 한다.
- 타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다.
행동 호환성 #
- 펭귄이 새가 아니라는 사실을 받아들이기 위한 출발점은 타입이 행동과 관련이 있다는 사실에 주목하는 것이다.
- 타입의 이름 사이에 개념적으로 어떤 연관성이 있다고 하더라도 행동에 연관성이 없다면 is-a 관계를 사용하지 말아야 한다.
- 결론은 두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 한다는 것이다.
- 행동의 호환 여부를 판단하는 기준은 클라이언트 관점이다.
- 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있다.
클라이언트 기대에 따라 계층 분리하기 #
- Bird → FlyingBird / Penguin
- 슈퍼타입인 Bird에서 fly메서드를 제외하면 펭귄은 새의 서브타입이 될 수 있다.
- fly 메서드가 있는 FlyingBird는 Bird의 서브타입으로 분리한다.
- 설계가 꼭 현실 세계를 반영할 필요는 없다.
서브클래싱과 서브 타이핑 #
- 서브 클래싱
- 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 가리킨다.
- 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.
- 서브 타이핑
- 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다.
- 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.
- 서브타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 한다. → 행동호환성
- 자식 클래스와 부모 클래스 사이의 행동호환성은 부모 클래스에 대한 자식 클래스의 대체 가능성을 포함한다.
04. 리스코프 치환 원칙 #
- 서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다.
- 클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용해야 한다.
- 리스코프 치환 원칙에 따르면 자식 클래스가 부모클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑이라고 불러야 한다.
클라이언트와 대체 가능성 #
- 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 강조
- 클라이언트와 격리한 채로 본 모델은 의미 있게 검증하는 것이 불가능하다.
- 어떤 모델의 유효성은 클라이언트의 관점에서만 검증 가능하다.
- 클라이언트 관점에서 Square가 Rectangle의 서브타입이 아닐 수 있음.
- Square의 setX, setY가 Rectangle과 호환이 안되는 예시
is-a 관계 다시 살펴보기 #
- 클라이언트 관점에서 자식 클래스의 행동이 부모 클래스의 행동과 호환되지 않고 그로 인해 대체가 불가능하다면 어휘적으로 is-a라고 말할 수 있다고 하더라도 그 관계는 is-a가 아님.
- 정사각형은 직사각형인가? 클라이언트에서 이를 동일하게 취급할 수 있을때만 그렇다.
- is-a관계는 객체지향에서 중요한 것은 객체의 속성이 아니라 객체의 행동이라는 점을 강조한다.
- 클라이언트를 고려하지 않은 채 개념과 속성의 측면에서 상속 관계를 정할 경우 리스코프 치환 원칙을 위반하는 서브클래싱에 이르게 될 확률이 높다.
- 행동을 고려하지 않은 두 타입의 이름이 단순히 is-a로 연결 가능하다고 해서 상속 관계로 연결하지 마라.
- 결론적으로 상속이 서브타이핑을 위해 사용될 경우에만 is-a관계다.
리스코프 치환 원칙은 유연한 설계의 기반이다. #
- 리스코프 치환 원칙은 클라이언트가 어떤 자식 클래스와도 안정적으로 협력할 수 있는 상속 구조를 구현할 수 있는 가이드라인을 제공한다.
- 새로운 자식 클래스를 추가하더라도 클라이언트의 입장에서 동일하게 행동하기만 하면 클라이언트를 수정하지 않고도 상속 계층을 확장할 수 있다.
- 클라이언트의 입장에서 퍼블릭 인터페이스의 행동 방식이 변경되지 않는다면 클라이언트의 코드를 변경하지 않고도 새로운 자식 클래스와 협력할 수 있게 된다는 것이다.
- 리스코프 치환 원칙을 따르는 설계는 유연할뿐만 아니라 확장성이 높다.
타입 계층과 리스코프 치환 원칙 #
- 클래스 상속은 타입 계층을 구현할 수 있는 다양한 방법 중 하나일 뿐이다.
- 구현 방법은 중요하지 않다. 핵심은 구현 방법과 무관하게 클라이언트의 관점에서 슈퍼타입에 대해 기대하는 모든 것이 서브타입에게도 적용돼야 하는 것이다.
5. 계약에 의한 설계와 서브타이핑 #
- 클라이언트와 서버 사이의 협력을 의무와 이익으로 구성된 계약의 관점에서 표현하는 것을 계약에 의한 설계라고 부른다.
- 계약에 의한 설계는 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 사전조건과 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 사후조건, 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 클래스 불변식의 세 가지 요소로 구성된다.
- 리스코프 치환 원칙은 어떤 타입이 서브타입이 되기 위해서는 슈퍼타입의 인스턴스와 협력하는 ‘클라이언트’의 관점에서 서브타입의 인스턴스가 슈퍼타입을 대체하더라도 협력에 지장이 없어야 한다는 것을 의미한다.
서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 '계약'을 준수해야 한다.
서브타입과 계약 #
- 서브타입에 더 강력한 사전조건을 정의할수는 없다.
- 서브타입에 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있다.
- 서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있다.
- 서브타입에 더 약한 사후조건을 정의할 수 없다.
- → 서브타입에 조건은 더 강해질 순 없지만, 리턴하는 타입의 범위는 좁혀도 괜찮음. 슈퍼타입과 호환되기 때문
- 부록 A 계약에 의한 설계를 참고