상속의 용도
- 타입 계층 구현
- 코드 재사용
📖 13.1 타입
프로그래밍 언어 관점에서의 타입과 개념 관점에서의 타입
🔖 13.1.1 개념 관점의 타입
개념 관점에서 타입이란 우리가 인지하는 세상의 사물의 종류를 의미한다.
어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스(instance)**라고 부른다.
- 타입의 인스턴스를 객체라고 부름.
- 심볼(symbol): 타입에 이름을 붙인 것
- 내연(intension): 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동
- 외연(extension): 타입에 속하는 객체들의 집합
🔖 13.1.2 프로그래밍 언어 관점의 타입
프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용하며 두 가지 목적이 있다.
- 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의
- 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공
타입은 적용 가능한 오퍼레이션의 종류와 의미를 정의함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용
🔖 13.1.3 객체지향 패러다임 관점의 타입
- 개념 관점에서 타입이란 공통의 특징을 공유하는 대상들의 분류다.
- 프로그래밍 언어 관점에서 타입이란 동일한 오퍼레이션을 적용할 수 있는 인스턴스들의 집합
객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것
- 객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일
- 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.
- 객체의 타입을 결정하는 것은 내부의 속성이 아니라 객체가 외부에 제공하는 행동이다.
📖 13.2 타입 계층
🔖 13.2.1 타입 사이의 포함관계
타입이 다른 타입에 포함될 수 있기 때문에 동일한 인스턴스가 하나 이상의 타입으로 분류되는 것도 가능
타입 계층을 구성하는 두 타입 관계에서 더 일반적인 타입을 **슈퍼타입(supertype)**이라고 부르고 더 특수한 타입을 **서브타입(subtype)**이라고 부른다.
내연 관점
- 일반화: 어떤 타입의 정의를 좀 더 보편적이고 추상적으로 만드는 과정을 의미
- 특수화: 어떤 타입의 정의를 좀 더 구체적이고 문맥 종속적으로 만드는 과정을 의미
외연 관점
- 슈퍼셋(superset): 일반적인 타입의 인스턴스 집합은 특수한 타입의 인스턴스 집합을 포함
- 서브셋(subset): 특수한 타입의 인스턴스 집합은 일반적인 타입의 인스턴스 집합에 포함
🔖 13.2.2 객체지향 프로그래밍과 타입 계층
- 슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의
- 서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의
서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.
📖 13.3 서브클래싱과 서브타이핑
🔖 13.3.1 언제 상속을 사용해야 하는가?
아래 두 질문에 모두 '예'라고 답할 수 있는 경우에만 상속을 사용
- 상속 관계가 is-a 관계를 모델링하는가?
- 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?
🔖 13.3.2 is-a 관계
어떤 타입 S가 다른 타입 T의 일종이라면 당연히 타입 S는 타입 T다라고 답할 수 있어야 한다.
타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다.
- ex. 펭귄은 새다, 새는 날 수 있다.
- 행동호환성이 더 중요
🔖 13.3.3 행동 호환성
타입의 이름 사이에 개념적으로 어떤 연관성이 있다고 하더라도 행동에 연관성이 없다면 is-a 관계를 사용하지 말아야 한다.
- 행동의 호환 여부를 판단하는 기준은 클라이언트의 관점이다.
public void flyBird(Bird bird) {
// 인자로 전달된 모든 bird들은 날 수 있어야 한다.
bird.fly();
}
public class Penguin extends Bird {
...
@Override
public void fly() {}
}
- 모든 bird가 날 수 있다는 클라이언트의 기대를 만족시키지 못한다.
public class Penguin extends Bird {
...
@Override
public void fly() {
throw new UnsupportedOperationException();
}
}
UnsupportedOperationException예외가 던져질 것이라고는 기대하지 않았을 것이다.
public void flyBird(Bird bird) {
// 인자로 전달된 모든 birtd가 Penguin의 인스턴스가 아닐 경우에만 fly() 메시지를 전송한다.
if (!(bird instanceof Penguin)) {
bird.fly();
}
}
- Penguin 이외에 날 수 없는 또 다른 새가 상속 계층에 추가 될 수도 있어 결합도를 높인다.
- 개방-폐쇄 원칙을 위반
🔖 13.3.4 클라이언트의 기대에 따라 계층 분리하기
public class Bird {
...
}
public class FlyingBird extends Bird {
public void fly() { ... }
...
}
public class Penguin extends Bird {
...
}
- 모든 클래스들이 행동호환성을 만족시킨다.
이 문제를 해결하는 다른 방법은 클라이언트에 따라 인터페이스를 분리하는 것이지만 더 좋은 방법은 합성을 사용하는 것이다.
자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중
🔖 13.3.5 서브클래싱과 서브타이핑
상속을 사용하는 두 가지 목적
-
**서브클래싱(subclassing)
- 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우
- 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.
- 구현상속(implementation inheritance) 또는 클래스 상속(class inheritance)**이라고 부른다.
-
**서브 타이핑(subtyping)
- 타입 계층을 구성하기 위해 상속을 사용하는 경우
- 서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.
- 이때 부모 클래스는 자식클래스의 슈퍼타입이 되고 자식 클래스는 부모 클래스의 서브타입이 된다.
- 인터페이스 상속(interface inheritance)이라고 부른다.
어떤 타입이 다른 타입의 서브타입이 되기 위해서는 행동 호환성을 만족시켜야 한다. 자식 클래스와 부모 클래스 사이의 행동 호환성은 부모 클래스에 대한 자식 클래스의 **대체 가능성(substitutability)**을 포함한다.
📖 13.4 리스코프 치환 원칙
서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다.
@AllArgsConstructor
public class Rectangle {
private final int x;
private final int y;
@Getter
@Setter
private int width;
@Getter
@Setter
private int height;
public int getArea() {
return width * height;
}
}
public class Square extends Rectangle {
public Square(int x, int y, int width, int height) {
super(x, y, width, height);
}
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width);
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
- Square의 너비와 높이는 항상 더 나중에 설정된 height의 값으로 설정된다.
- Square는 Rectangle의 구현을 재사용하고 있을 뿐이다. 즉, 서브클래싱 관계다.
🔖 13.4.1 클라이언트와 대체 가능성
클라이언트와 격리한 채로 본 모델은 의미 있게 검증하는 것이 불가능하다.
- 리스코프 치환 원칙은 상속 관계에 있는 두 클래스 사이의 관계를 클라이언트와 떨어트려 놓고 판단하지 말라고 속삭인다.
🔖 13.4.2 is-a 관계 다시 살펴보기
클라이언트 관점에서 자식 클래스의 행동이 부모 클래스의 행동과 호환되지 않고 그로 인해 대체가 불가능하다면 어휘적으로 is-a라고 말할 수 있다고 하더라도 그 관계를 is-a 관계라고 할 수 없다.
- 상속이 서브타이핑을 위해 사용될 경우에만 is-a 관계
🔖 13.4.3 리스코프 치환 원칙은 유연한 설계의 기반이다
리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다.
- 자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어진다.
🔖 13.4.4 타입 계층과 리스코프 치환 원칙
클래스 상속은 타입 계층을 구현할 수 있는 다양한 방법 중 하나일 뿐이다.
- 구현 방법과 무관하게 클라이언트의 관점에서 슈퍼타입에 대해 기대하는 모든 것이 서브타입에게도 적용돼야 한다.
📖 13.5 계약에 의한 설계와 서브타이핑
계약에 의한 설계(Design By Contract, DBC)
- 클라이언트와 서버 사이의 협력을 의무와 이익으로 구성된 계약의 관점에서 표현하는 것
- 사전조건: 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건
- 사후조건: 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 조건
- 클래스 불변식: 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 것
서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 '계약'을 준수해야 한다.
🔖 13.5.1 서브타입과 계약
어떤 타입이 슈퍼타입에서 정의한 사전조건보다 더 약한 사전조건을 정의하고 있다면 그 타입은 서브타입이 될 수 있지만 더 강한 사전조건을 정의한다면 서브타입이 될 수 없다.
계약에 의한 설계는 클라이언트 관점에서의 대체 가능성을 계약으로 설명할 수 있다는 사실을 잘 보여준다.
