> For the complete documentation index, see [llms.txt](https://develop-footprint.gitbook.io/o/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://develop-footprint.gitbook.io/o/programming/object/13.md).

# 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)하여 영향을 더 세밀하게 제어할 수 있다.

<figure><img src="/files/f7v55pyAtdB0NFJQRuW3" alt=""><figcaption><p>상속 계층을 분리</p></figcaption></figure>

<figure><img src="/files/TtuYB9OtYEWUmZbdKMqQ" alt=""><figcaption><p>인터페이스를 분리</p></figcaption></figure>

* 설계가 꼭 현실 세계를 반영할 필요는 없다. 중요한 것은 설계가 반영할 **도메인의 요구사항**이고 그 안에서 클라이언트가 객체에게 요구하는 행동이다.
* 서브클래싱
  * 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 가리킨다.
  * 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.
  * 구현 상속(implementation inheritance) 또는 클래스 상속(class inheritance)이라고 부르기도 한다.
* 서브타이핑
  * 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킨다.
  * 자식 클래스와 부모 클래스의 **행동이 호환**되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.
  * 인터페이스 상속(interface inheritance)이라고 부르기도 한다.
* 모든 서브클래스가 서브타입인 것은 아니다.

## 리스코프 치환 원칙

* 서브타입은 그것의 기반 타입을 대체할 수 있어야 한다.
* 사각형이라는 클래스를 정사각형이 상속받고, 클라이언트가 사각형을 입력받아 크기를 변형하는 로직을 구현한다면 아래와 같이 에러가 발생할 수 있다.

```java
public void resizeMyRectangle(Rectangle rectangle, int width, int height) {
	rectangle.setWidth(width);
	rectangle.setHeight(height);
	// 정사각형을 입력받아 width, height를 다르게 입력하여도 둘 중 하나만 적용되므로 Error 발생!
	assert rectangle.getWidth() == width && rectangle.getHeight() == height;
} 

public class Rectangle {
	private int x, y, width, height;
	public int getWidth() {
		return width;
	}
	public void setWidth(int width) {
		this.width = width;
	}
	public int getHeight() {
		return height;
	}
	public void setHeight(int height) {
		this.height = height;
	}
	public int getArea() {
		return width * height;
	}
}

public class Square extends Rectangle {
	@Override
	public void setWidth(int width) {
		super.setWidth(width);
		super.setHeight(width);
	}
	@Override
	public void setHeight(int height) {
		super.setWidth(height);
		super.setHeight(height);
	}
}
```

* 현실속에서는 “정사각형은 직사각형이다” 라는 is-a 관계가 만족하지만, 클라이언트 관점에서 행동이 호환되지 않으므로 자식 클래스는 부모 클래스 대신 사용할 수 없게 된다.
* 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다.
* 클라이언트와 격리한 채로 보는 모델은 의미 있게 검증하는 것이 불가능하다. 상속 관계는 **클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 있을 때**만 올바르다.
* is-a는 **클라이언트 관점에서 is-a일 때**만 참이다.
* 슈퍼타입과 서브타입이 클라이언트 입장에서 행동이 호환된다면, 두 타입을 is-a로 연결해 문장을 만들어도 어색하지 않은 단어로 타입의 이름을 정하는 것이 좋다.
* 리스코프 치환 원칙은 기능 확장을 하면서 기존 코드를 수정할 필요는 없는 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다.
* 타입 계층을 구현하는 방법에는 상속 외에 인터페이스도 있다.

## 계약에 의한 설계와 서브타이핑

* 계약에 의한 설계(Design By Contract, DBC)란, 클라이언트와 서버 사이의 협력을 의무(obligation)와 이익(benefit)으로 구성된 계약의 관점에서 표현하는 것이다.
* 아래와 같이 세 가지 요소로 구성된다.
  * 사전조건(precondition): 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건
  * 사후조건(postcondition): 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 조건
  * 클래스 불변식(class invariant): 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 조건
* 서브타입에는 슈퍼타입보다 더 강력한 사전조건은 정의할 수 없다. 대신 서브타입에는 슈퍼타입과 **같거나 더 약한 사전조건**을 정의할 수 있다. 클라이언트가 슈퍼 타입의 사전 조건에 맞게 구성한 곳에 서브 타입을 넣으면 정상적으로 돌아가지 않을 것이기 있기 때문이다.
* 마찬가지로 서브타입에 슈퍼타입과 **같거나** **더 강한 사후조건**만 정의할 수 있다.
* 서브타이핑을 위해 상속을 사용한다면 부모 클래스가 클라이언트와 맺고 있는 계약과의 관계를 잘 확인해야 한다.
