BottleH Blog

Object - 13장 서브클래싱과 서브타이핑

    Tags

  • java
Object - 13장 서브클래싱과 서브타이핑 thumbnail

상속의 용도

  1. 타입 계층 구현
  2. 코드 재사용

📖 13.1 타입

프로그래밍 언어 관점에서의 타입과 개념 관점에서의 타입

🔖 13.1.1 개념 관점의 타입

개념 관점에서 타입이란 우리가 인지하는 세상의 사물의 종류를 의미한다.

어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스(instance)**라고 부른다.

  • 타입의 인스턴스를 객체라고 부름.
  • 심볼(symbol): 타입에 이름을 붙인 것
  • 내연(intension): 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동
  • 외연(extension): 타입에 속하는 객체들의 집합

🔖 13.1.2 프로그래밍 언어 관점의 타입

프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용하며 두 가지 목적이 있다.

  1. 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의
  2. 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공

타입은 적용 가능한 오퍼레이션의 종류와 의미를 정의함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용

🔖 13.1.3 객체지향 패러다임 관점의 타입

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

객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것

  • 객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일
  • 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.
  • 객체의 타입을 결정하는 것은 내부의 속성이 아니라 객체가 외부에 제공하는 행동이다.

📖 13.2 타입 계층

🔖 13.2.1 타입 사이의 포함관계

타입이 다른 타입에 포함될 수 있기 때문에 동일한 인스턴스가 하나 이상의 타입으로 분류되는 것도 가능

타입 계층을 구성하는 두 타입 관계에서 더 일반적인 타입을 **슈퍼타입(supertype)**이라고 부르고 더 특수한 타입을 **서브타입(subtype)**이라고 부른다.

내연 관점

  • 일반화: 어떤 타입의 정의를 좀 더 보편적이고 추상적으로 만드는 과정을 의미
  • 특수화: 어떤 타입의 정의를 좀 더 구체적이고 문맥 종속적으로 만드는 과정을 의미

외연 관점

  • 슈퍼셋(superset): 일반적인 타입의 인스턴스 집합은 특수한 타입의 인스턴스 집합을 포함
  • 서브셋(subset): 특수한 타입의 인스턴스 집합은 일반적인 타입의 인스턴스 집합에 포함

🔖 13.2.2 객체지향 프로그래밍과 타입 계층

  • 슈퍼타입이란 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의
  • 서브타입이란 슈퍼타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의

서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.

📖 13.3 서브클래싱과 서브타이핑

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

아래 두 질문에 모두 '예'라고 답할 수 있는 경우에만 상속을 사용

  1. 상속 관계가 is-a 관계를 모델링하는가?
  2. 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?

🔖 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 서브클래싱과 서브타이핑

상속을 사용하는 두 가지 목적

  1. **서브클래싱(subclassing)

    • 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우
    • 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.
    • 구현상속(implementation inheritance) 또는 클래스 상속(class inheritance)**이라고 부른다.
  2. **서브 타이핑(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 서브타입과 계약

어떤 타입이 슈퍼타입에서 정의한 사전조건보다 더 약한 사전조건을 정의하고 있다면 그 타입은 서브타입이 될 수 있지만 더 강한 사전조건을 정의한다면 서브타입이 될 수 없다.

계약에 의한 설계는 클라이언트 관점에서의 대체 가능성을 계약으로 설명할 수 있다는 사실을 잘 보여준다.

Written by@BottleH
Back-End Developer

GitHub