• oop
  • design
  • java
  • book

오브젝트 Ch.13 — 서브클래싱과 서브타이핑

is-a 관계의 한계와 행동 호환성, 인터페이스 분리 원칙, 리스코프 치환 원칙으로 올바른 타입 계층의 기준을 정리한다.

시리즈 · BackEnd 서적17 / 17
  1. SQL 레벨업 Ch.1 — DBMS 아키텍처
  2. SQL 레벨업 Ch.3 — SQL의 조건 분기
  3. SQL 레벨업 Ch.4~5 — 집약과 자르기, 반복문
  4. SQL 레벨업 Ch.6 — 결합
  5. SQL 레벨업 Ch.7 — 서브쿼리
  6. SQL 레벨업 Ch.8 — SQL의 순서
  7. SQL 레벨업 Ch.9 — 갱신과 데이터 모델
  8. SQL 레벨업 Ch.10 — 인덱스 사용
  9. 오브젝트 Ch.1 — 객체, 설계
  10. 오브젝트 Ch.2 — 객체지향 프로그래밍
  11. 오브젝트 Ch.3~4 — 역할, 책임, 협력 / 설계 품질과 트레이드오프
  12. 오브젝트 Ch.5 — 책임 할당하기
  13. 오브젝트 Ch.6 — 메시지와 인터페이스
  14. 오브젝트 Ch.8 — 의존성 관리하기
  15. 오브젝트 Ch.9 — 유연한 설계
  16. 오브젝트 Ch.10, 12 — 상속과 코드 재사용, 다형성
  17. 오브젝트 Ch.13 — 서브클래싱과 서브타이핑

상속의 용도는 두 가지다.

  • 타입 계층 구현: 부모 클래스는 일반적인 개념을, 자식 클래스는 특수한 개념을 구현한다. 부모는 자식의 일반화이고 자식은 부모의 특수화다.
  • 코드 재사용: 간단하게 재사용할 수 있지만 결합도가 강해 변경하기 어려운 코드를 얻을 확률이 높다. (10장에서 정리했다.)

상속을 사용하는 일차적인 목표는 타입 계층을 구현하는 것이어야 한다. 13장은 올바른 타입 계층을 구성하는 원칙을 다룬다.

타입

타입은 세 가지 요소로 구성된다.

  • 심볼(symbol): 타입에 붙인 이름
  • 내연(intension): 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동
  • 외연(extension): 타입에 속하는 객체들의 집합

개념 관점에서 타입은 공통의 특징을 공유하는 대상들의 분류이고, 프로그래밍 언어 관점에서는 오퍼레이션을 적용할 수 있는 인스턴스들의 집합이다.

객체지향에서 오퍼레이션은 객체가 수신할 수 있는 메시지다. 따라서 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이고, 그 메시지의 집합이 퍼블릭 인터페이스다. 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.

서브타입과 슈퍼타입

타입 계층에서 더 일반적인 타입을 슈퍼타입(supertype), 더 특수한 타입을 서브타입(subtype)이라고 부른다.

서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다. 이것이 상속과 다형성의 관계를 이해하는 출발점이다.

언제 상속을 사용해야 하는가

마틴 오더스키는 다음 두 질문에 모두 ’예’라고 답할 수 있을 때에만 상속을 사용하라고 조언한다.

  1. 상속 관계가 is-a 관계를 모델링하는가? “[자식 클래스]는 [부모 클래스]다”라고 말해도 이상하지 않다면 상속을 사용할 후보로 볼 수 있다.
  2. 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가? 클라이언트는 부모와 자식의 차이점을 몰라야 한다. 이를 행동 호환성이라고 한다.

첫 번째보다 두 번째 질문에 초점을 맞추는 것이 중요하다.

is-a 관계의 한계

  • 펭귄은 새다.
  • 새는 날 수 있다.

펭귄은 새지만, 새의 정의에 ’날 수 있다’는 행동이 포함된다면 펭귄은 새의 서브타입이 될 수 없다. 그 행동이 포함되지 않는다면 서브타입이 될 수 있다. 타입 계층의 의미는 행동이라는 문맥에 따라 달라진다. 그래서 is-a 관계가 성립하더라도 후보로만 생각해야 한다.

행동 호환성

타입의 이름 사이에 개념적인 연관성이 있더라도 행동에 연관성이 없다면 타입 계층으로 묶지 말아야 한다.

  • 행동의 호환 여부를 판단하는 기준은 동일한 메서드를 구현하고 있는지가 아니라 클라이언트의 관점이다.
  • 클라이언트가 두 타입이 동일하게 행동할 것이라 기대하지 않는다면 두 타입을 타입 계층으로 묶어서는 안 된다.

펭귄이 새의 서브타입이 아닌 이유는 클라이언트가 모든 새는 날 수 있다고 가정하기 때문이다.

인터페이스 분리 원칙 (ISP)

해결 방법은 클라이언트의 기대에 맞게 상속 계층을 분리하는 것이다.

public class Bird {
    // ...
}

public class FlyingBird extends Bird {
    public void fly() { /* ... */ }
}

public class Penguin extends Bird {
    // ...
}

// 날 수 있는 새에게만 메시지를 보낸다
public void flyBird(FlyingBird bird) {
    bird.fly();
}
  • 날 수 없는 새에게 걸으라는 메시지를 보내야 한다면? fly 오퍼레이션을 가진 Flyer 인터페이스와 walk 오퍼레이션을 가진 Walker 인터페이스로 분리한다.
  • Penguin이 Bird의 코드를 재사용해야 한다면? 상속이 아닌 합성으로 해결한다.

이처럼 클라이언트의 기대에 따라 인터페이스를 분리해 변경의 영향을 제어하는 설계 원칙이 인터페이스 분리 원칙(Interface Segregation Principle)이다.

서브클래싱과 서브타이핑

상속을 사용하는 목적에 따라 둘로 나뉜다.

서브클래싱 (subclassing)

  • 코드를 재사용할 목적으로 상속을 사용하는 경우다.
  • 자식과 부모의 행동이 호환되지 않으므로 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.
  • 구현 상속(implementation inheritance) 또는 클래스 상속(class inheritance)이라고도 한다.

서브타이핑 (subtyping)

  • 타입 계층을 구성하기 위해 상속을 사용하는 경우다.
  • 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다. 부모는 자식의 슈퍼타입, 자식은 부모의 서브타입이 된다.
  • 인터페이스 상속(interface inheritance)이라고도 한다.

리스코프 치환 원칙 (LSP)

서브타입은 그것의 기반 타입에 대해 대체 가능해야 한다. 클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다.

클라이언트 입장에서 퍼블릭 인터페이스의 행동 방식이 변하지 않는다면, 클라이언트 코드를 변경하지 않고도 새로운 자식 클래스와 협력할 수 있다. 그래서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계의 전제 조건이다. 리스코프 치환 원칙 위반은 잠재적인 개방-폐쇄 원칙 위반이다.

서브타입과 계약

  • 서브타입에 슈퍼타입보다 강한 사전조건을 정의할 수 없다. 같거나 더 약한 사전조건은 가능하다.
  • 서브타입에 슈퍼타입보다 약한 사후조건을 정의할 수 없다. 같거나 더 강한 사후조건은 가능하다.