13장: 서브클래싱과 서브타이핑
상속의 목표
상속을 사용하는 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이어야 한다.
코드 재사용을 목표로 상속을 할 경우 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 설계의 변경과 진화를 방해한다.
타입 계층을 목표로 상속을 사용하면 다형적으로 동작하는 객체들의 관계에 기반해 확장 가능하고 유연한 설계를 얻을 수 있게 된다.
객체 지향 프로그래밍 vs 객체 기반 프로그래밍
객체지향 프로그래밍은 클래스 사이의 상속과 다형성을 지원하지만 객체기반 프로그래밍은 지원하지 않는다.
객체 기반 프로그래밍은 클래스가 존재하지 않는 프로토타입 기반 언어(Prototype-Based Language)를 사용한 프로그래밍 방식을 의미한다.
타입
개념 관점의 타입
우리가 인지하는 세상의 사물의 종류
예를 들어 Java, Python은 프로그래밍 언어라는 타입의 인스턴스(객체)이다.
심볼(symbol)이란 타입에 이름을 붙인 것이다.
내연(intension)이란 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동을 가리킨다.
외연(extension)이란 타입에 속하는 객체들의 집합이다.
프로그래밍 언어 관점의 타입
비트 묶음에 의미를 부여하기 위해 정의된 제약과 규칙
타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의하거나, 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공하기 위해 사용된다.
객체지향 패러다임 관점의 타입
객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.
객체가 수신할 수 있는 메시지를 기준으로 타입을 분류하기 때문에, 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다.
어떤 객체들이 동일한 상태를 가지고 있더라도 퍼블릭 인터페이스가 다르다면 이들은 서로 다른 타입으로 분류된다.
타입 계층
다른 타입을 포함하는 타입은 포함되는 타입보다 더 많은 인스턴스를 가진다. 예를 들어 과일 타입에 포함되는 열대 과일 타입에는 과일 타입보다 적은 인스턴스를 갖게 된다.
포함하는 타입은 외연 관점에서는 더 크고 내연 관점에서는 더 일반적이다. 포함되는 타입은 외연 관점에서는 더 작고 내연 관점에서는 더 특수하다.
두 타입 간의 관계에서 더 일반적인 타입을 슈퍼타입이라 하고 더 구체적이고 특수한 타입을 서브타입이라 한다.
일반화란 어떤 타입의 정의를 좀 더 보편적이고 추상적으로 만드는 과정을 의미한다.
특수화란 어떤 타입의 정의를 좀 더 구체적이고 문맥 종속적으로 만드는 과정을 의미한다.
객체지향 프로그래밍 관점에서의 타입 계층
객체의 타입을 결정하는 것은 퍼블릭 인터페이스이다.
슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것이다.
서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한 것이다.
서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.
서브클래싱과 서브타이핑
상속을 이용해 타입 계층을 구현한다는 것은 부모 클래스가 슈퍼타입의 역할을, 자식 클래스가 서브타입의 역할을 수행하도록 클래스 사이의 관계를 정의한다는 것을 의미한다.
상속을 사용하려면 두 클래스의 관계가 is-a 관계를 모델링하며, 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방해야 한다.
is-a 관계
어떤 타입 S가 다른 타입 T의 일종이라면 당연히 “타입 S는 타입 T다(S is-a T)”라고 말할 수 있어야 한다.
펭귄은 새이다 라는 생각으로 펭귄이 새를 상속받도록 하면, 새에 존재하는 “날 수 있다” 라는 특성을 사용하기가 불가능해진다. 따라서 기대되는 행동에 따라 타입 계층을 구성해야 한다.
어떤 두 대상을 언어적으로 is-a라고 표현할 수 있더라도 일단은 상속을 사용할 예비 후보 정도로만 생각하고, 두 개념이 어떤 방식으로 사용되고 협력하는지 살펴보며 행동 호환성이 가능한지 확인해야 한다.
행동 호환성
상속 계층을 사용하는 클라이언트는 부모 클래스와 자식 클래스의 차이를 몰라야 하며 행동 호환성이 보장되어야 한다.
타입의 이름 사이에 개념적으로 어떤 연관성이 있다고 하더라도, 행동에 연관성이 없다면 상속 관계를 사용하지 말아야 한다.
행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다. 클라이언트가 두 타입이 동일하게 행동하지 않을 것이라고 기대한다면 두 타입을 타입 계층으로 묶어서는 안 된다.
행동 호환성이 없지만 상속을 유지하기 위해서는 메서드가 아무것도 하지 않도록 비워두거나, UnsupportedOperationException을 반환하거나, 인스턴스를 확인하여 특정 인스턴스일 때에만 메서드를 호출하도록 할 수 있다. 하지만 이렇게 둘 경우 구체 클래스에 대한 결합도를 높이고 개방 폐쇄 원칙을 위반할 가능성이 높아진다.
이를 해결하기 위해선 클라이언트가 원하는 행동에 대한 기대에 따라 상속 계층을 분리하여 행동 호환성을 만족시키거나, 클라이언트에 따라 인터페이스를 분리(Interface Segregation Principle)하여 영향을 더 세밀하게 제어할 수 있다.
설계가 꼭 현실 세계를 반영할 필요는 없다. 중요한 것은 설계가 반영할 도메인의 요구사항이고 그 안에서 클라이언트가 객체에게 요구하는 행동이다.
서브클래싱
다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 가리킨다.
자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.
구현 상속(implementation inheritance) 또는 클래스 상속(class inheritance)이라고 부르기도 한다.
서브타이핑
타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다.
자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.
인터페이스 상속(interface inheritance)이라고 부르기도 한다.
모든 서브클래스가 서브타입인 것은 아니다.
리스코프 치환 원칙
서브타입은 그것의 기반 타입을 대체할 수 있어야 한다.
사각형이라는 클래스를 정사각형이 상속받고, 클라이언트가 사각형을 입력받아 크기를 변형하는 로직을 구현한다면 아래와 같이 에러가 발생할 수 있다.
현실속에서는 “정사각형은 직사각형이다” 라는 is-a 관계가 만족하지만, 클라이언트 관점에서 행동이 호환되지 않으므로 자식 클래스는 부모 클래스 대신 사용할 수 없게 된다.
자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다.
클라이언트와 격리한 채로 보는 모델은 의미 있게 검증하는 것이 불가능하다. 상속 관계는 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 있을 때만 올바르다.
is-a는 클라이언트 관점에서 is-a일 때만 참이다.
슈퍼타입과 서브타입이 클라이언트 입장에서 행동이 호환된다면, 두 타입을 is-a로 연결해 문장을 만들어도 어색하지 않은 단어로 타입의 이름을 정하는 것이 좋다.
리스코프 치환 원칙은 기능 확장을 하면서 기존 코드를 수정할 필요는 없는 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다.
타입 계층을 구현하는 방법에는 상속 외에 인터페이스도 있다.
계약에 의한 설계와 서브타이핑
계약에 의한 설계(Design By Contract, DBC)란, 클라이언트와 서버 사이의 협력을 의무(obligation)와 이익(benefit)으로 구성된 계약의 관점에서 표현하는 것이다.
아래와 같이 세 가지 요소로 구성된다.
사전조건(precondition): 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건
사후조건(postcondition): 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 조건
클래스 불변식(class invariant): 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 조건
서브타입에는 슈퍼타입보다 더 강력한 사전조건은 정의할 수 없다. 대신 서브타입에는 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있다. 클라이언트가 슈퍼 타입의 사전 조건에 맞게 구성한 곳에 서브 타입을 넣으면 정상적으로 돌아가지 않을 것이기 있기 때문이다.
마찬가지로 서브타입에 슈퍼타입과 같거나 더 강한 사후조건만 정의할 수 있다.
서브타이핑을 위해 상속을 사용한다면 부모 클래스가 클라이언트와 맺고 있는 계약과의 관계를 잘 확인해야 한다.
Last updated