Post

서브클래싱과 서브타이핑

13. 서브클래싱과 서브타이핑

상속의 첫 번째 용도는 타입 계층을 구현하는 것

타입 계층 안에서 부모 클래스는 일반적인 개념을 구현하고 자식 클래스는 특수한 개념을 구현한다.

타입 계층의 관점에서 부모 클래스는 자식 클래스의 일반화(generilization)이고 자식 클래스는 부모 클래스의 특수화(specialization)

상속의 두 번째 용도는 코드 재사용

상속은 간단한 선언만으로 부모 클래스의 코드를 재사용 가능

하지만 재사용을 위해 상속을 사용할 경우 부모 클래스와 자식 클래스가 강하게 결합되기 때문에 변경하기 어려운 코드를 얻게 될 확률이 높다.

상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것

상속은 코드를 쉽게 재사용할 수 있는 방법을 제공하지만 부모 클래스와 자식 클래스를 강하게 결합시기키 때문에 설계의 변경과 진화를 방해

반면 타입 계층을 목표로 상속을 사용하면 다형적으로 동작하는 객체들의 관계에 기반해 확장 가능하고 유연한 설계를 얻을 수 있게 됨

동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 함

객체지향 프로그래밍과 객체기반 프로그래밍

객체기반 프로그래밍(Object-Based Programming)이란 상태와 행동을 캡슐화한 객체를 조합해서 프로그램을 구성하는 방식

객체지향 프로그래밍 역시 객체기반 프로그래밍의 한 종류

객체지향 프로그래밍은 객체기반 프로그래밍과 마찬가지로 객체들을 조합해서 애플리케이션을 개발하지만 상속다형성을 지원한다는 점에서 객체기반 프로그래밍과 차별됨

13.1 타입

개념 관점의 타입

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

우리가 인식하는 객체들에 적용하는 개념이나 아이디어

타입은 사물을 분류하기 위한 틀로 사용됨

어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스(instance)

일반적으로 타입의 인스턴스를 객체라고 부름

타입은 심볼, 내연, 외연의 세 가지 요소로 구성

  • 심볼(symbol): 타입에 이름을 붙인 것, ex. 프로그래밍 언어
  • 내연(intension): 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동 ex. ‘컴퓨터에게 특정한 작업을 지시하기 위한 어휘와 문법적 규칙의 집합’
  • 외연(extension): 타입에 속하는 객체들의 집합, ex. 자바, 루비, 자바스크립트, C

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

프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용

프로그래밍 언어에서 타입은 두 가지 목적을 위해 사용됨

  1. 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의

    자바에서 ‘+’ 연산자는 원시형 숫자 타입이나 문자열 타입의 객체에는 사용할 수 있지만 다른 클래스의 인스턴스에 대해서는 사용 불가능

  2. 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공

    자바에서 a + b라는 연산이 있을 때 a와 b의 타입이 int라면 두 수를 더함

    하지만 a와 b가 문자열이라면 두 문자열을 하나의 문자열로 합칩

    따라서 a와 b에 부여된 타입이 ‘+’ 연산자의 문맥을 정의

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

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

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

객제지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미

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

객체가 수신할 수 있는 메시지의 집합을 가리키는 멋진 용어 -> 퍼블릭 인터페이스

객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일

객체지항에서는 객체가 수신할 수 있는 메시지를 기준으로 타입을 분류하기 때문에 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류 가능

객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류

객체에게 중요한 것은 속성이 아니라 행동

어떤 객체들이 동일한 상태를 가지고 있더라도 퍼블릭 인터페이스가 다르다면 이들은 서로 다른 타입

어떤 객체들이 내부 상태는 다르지만 동일한 퍼블릭 인터페이스를 공유한다면 이들은 동일한 타입으로 분류

13.2 타입 계층

타입 사이의 포함 관계

타입 역시 객체들의 집합이기 때문에 다른 타입을 포함하는 것이 가능

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

다른 타입을 포함하는 타입은 포함되는 타입보다 좀 더 일반화된 의미를 표현 가능

반면 포함되는 타입은 좀 더 특수하고 구체적

다른 타입을 포함하는 타입은 포함되는 타입보다 더 많은 인스턴스를 가짐

포함하는 타입은 외연 관점에서는 더 크고 내연 관점에서는 더 일반적

포함되는 타입은 외연 관점에서는 더 작고 내연 관점에서는 더 특수함

타입 계층을 구성하는 두 타입 간의 관계에서 더 일반적인 타입을 슈펴 타입(supertype)

더 특수한 타입을 서브타입(subtype)

객체의 정의를 의미하는 내연 관점에서 일반화란 어떤 타입의 정의를 좀 더 보편적이고 추상적으로 만드는 과정

특수화란 어떤 타입의 정의를 좀 더 구체적이고 문맥 종속적으로 만드는 과정

내연의 관점에서 특수한 타입의 정의는 일반적인 타입의 정의를 좀 더 구체화한 것

집합을 의미하는 외연의 관점에서 일반적인 타입의 인스턴스 집합은 특수한 타입의 인스턴스 집합을 포함하는 슈퍼셋(superset)

반대로 특수한 타입의 인스턴스 집합은 일반적인 타입의 인스턴스 집합에 포함된 서브셋(subset)

따라서 특수한 타입에 속한 인스턴스는 동시에 더 일반적인 타입의 인스턴스이기도 하다

일반화: 다른 타입을 완전히 포함하거나 내포하는 타입을 식별하는 행위 또는 그 행위의 결과

특수화: 다른 타입 안에 전체적으로 포함되거나 완전히 내포되는 타입을 식별하는 행위 또는 그 행위의 결과

슈퍼타입은 다음과 같은 특징을 가지는 타입

  • 집합이 다른 집합의 모든 멤버를 포함
  • 타입 정의가 다른 타입보다 좀 더 일반적

서브타입은 다음과 같은 특징을 가지는 타입

  • 집합에 포함되는 인스턴스들이 더 큰 집합에 포함
  • 타입 정의가 다른 타입보다 좀 더 구체적

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

객체의 타입을 결정하는 것은 퍼블릭 인터페이스

일반적인 타입이란 비교하려는 타입에 속한 객체들의 퍼블릭 인터페이스보다 더 일반적인 퍼블릭 인터페이스를 가지는 객체들의 타입

특수한 타입이란 비교하려는 타입에 속한 객체들의 퍼블릭 인터페이스보다 더 특수한 퍼블릭 인터페이스를 가지는 객체들의 타입

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

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

서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주 가능

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

객체지향 프로그래밍 언어에서 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것

타입 계층을 구현하는 일반적인 방법은 상속을 이용하는 것

상속을 이용해 타입 계층을 구현한다는 것은 부모 클래스가 슈퍼타입의 역할을, 자식 클래스가 서브타입의 역할을 수행하도록 클래스 사이의 관계를 정의하는 것을 의미

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

상속의 올바른 용도는 타입 계층을 구현하는 것

어떤 조건을 만족시켜야만 타입 계층을 위해 올바르게 상속을 사용했다고 말할 수 있을까?

  1. 상속 관계가 is-a 관계를 모델링하는가?

    일반적으로 “[자식클래스]는 [부모 클래스]다”라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주 가능

  2. 클라이언트의 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?

    클라이언트의 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 한다. -> 행동 호환성

is-a 관계

두 클래스가 어휘적으로 is-a 관계를 모델링한 경우에만 상속을 사용해야 함

하지만 is-a 관계가 생각처럼 직관적이고 명쾌하지 않음

ex. “펭귄은 새다”, “새는 날 수 있다.”

어휘적인 정의가 아니라 기대되는 행동에 따라 타입 계층을 구성해야 한다는 사실을 보여줌

따라서 타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다.

슈퍼타입과 서브타입 관계에서는 is-a보다 행동 호환성이 더 중요

어떤 두 대상을 언어적으로 is-a 관계라고 표현할 수 있더라도 일단은 상속을 사용할 예비 후보 정도로만 생각하라

행동 호환성

두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 한다.

행동의 호환 여부를 판단하는 기준은 클라이언트의 관점

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

타입 계층을 이해하기 위해서는 그 타입 계층이 사용될 문맥을 이해하는 것이 중요

상속 관계를 유지하면서 이 문제를 해결하기 위한 시도

  1. Penguin의 fly 메서드를 오버라이딩해서 내부 구현을 비워둠

    하지만 이 방법은 어떤 행동도 수행하지 않기 때문에 모든 bird가 날 수 있다는 클라이언트의 기대를 만족시키지 못함

    따라서 올바른 설계 X -> 호환 X -> 올바른 타입 계층 X

  2. Penguin의 fly 메서드를 오버라이딩한 후 예외를 던지게 하는 것

    이 방법 역시 클라이언트의 관점에서 행동이 호환되지 않음

  3. 메서드를 수정하여 인자로 전달된 bird의 타입이 Penguin이 아닌 경우에만 fly 메시지 전송

    Penguin 이외에 날 수 없는 새가 또 추가 된다면?? -> 예외처리 코드 계속 추가 -> 결합도가 높아짐 -> 개방-폐쇄 원칙 위반

클라이언트의 기대애 따라 계층 분리하기

날 수 있는 새와 날 수 없는 새를 명확하게 구분할 수 있게 상속 계층을 분리하면 서로 다른 요구사항을 가진 클라이언트를 만족시킬 수 있음

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public clss Bird {
  ...
}

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

public class Penguin extends Bird {
  ...
}

변경 후에는 모든 클래스들이 행동 호환성을 만족시킴

이 문제를 해결하는 다른 방법은 클라이언트에 따라 인터페이스를 분리하는 것

fly와 walk가 필요한 경우 fly를 구현한 인터페이스와 walk를 구현한 인터페이스 분리

bird는 flyer와 walker 모두 상속, penguin은 walker만 상속

클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어 가능

인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙 -> 인터페이스 분리 원칙(Interface Segmentation Principle, ISP)

이 원칙은 비대한 인터페이스의 단점을 해결

비대한 인터페이스를 가지는 클래스는 응집성이 없는 인터페이스를 가지는 클래스

비대한 클래스는 그 클라이언트 사이에 이상하고 해로운 결합이 생기게 만든다.

비대한 클래스에 변경을 가하면, 나머지 모든 클래스가 영향을 받게 됨

그래서 분리가 필요

요점은 자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중

서브클래싱과 서브타이핑

언제 상속을 사용해야 하는가? -> 코드 재사용, 타입 계층 구성

상속을 사용하는 두 가지 목적 -> 서브클래싱서브타이핑

  • 서브클래싱(subclassing): 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우

    자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없음

    서브클래싱을 구현 상속(implementation inheritance) 또는 클래스 상속(class inheritance)

  • 서브타이핑(subtyping): 타입 계층을 구성하기 위해 상속을 사용하는 경우

    자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스를 부모 클래스의 인스턴스로 대체 가능

    인터페이스 상속(interface inheritance)이라고 부름

타입을 설명할 때 가장 중요한 것은 퍼블릭 인터페이스

개념젹으로 서브타입이 슈퍼타입의 퍼블릭 인터페이스를 상속받는 것처럼 보이게 된다.

그래서 서브타이핑을 인터페이스 상속이라고 부름

자식 클래스와 부모 클래스 사이의 행동 호환성은 부모 클래스에 대한 자식 클래스의 대체 가능성(substitutability)을 포함

행동 호환성과 대체 가능성은 올바른 상속 관계를 구축하기 위해 따라야 할 지침 -> 리스코프 치환 원칙

13.4 리스코프 치환 원칙

상속 관계로 연결한 두 클래스가 서브타이핑 관계를 만족시키기 위해서는 다음의 조건을 만족시켜야 함

S형의 각 객체 o1에 대해 T형의 객체 o2가 하나 있고, T에 의해 정의된 모든 프로그램P에서 T가 S로 치환될 때, P의 동작이 변하지 않으면 S는 T의 서브타입

자식 클래스가 부모 클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브타이핑이라고 불러야 함

Square와 Rectangle에서 resize가 높이와 가로가 다르다고 가정

-> Square는 Rectangle의 서브타이핑이 아니라 서브클래싱 관계

클라이언트와 대체 가능성

Square가 Rectangle을 대체할 수 없는 이유는 클라이언트의 관점에서 Square와 Rectangle이 다르기 때문

리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 강조

클라이언트와 격리한 채로 본 모델 의미 있게 검증하는 것이 불가능

상속 관계는 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 있을 때만 올바르다.

is-a 관계 다시 살퍼보기

is-a 관계로 표현된 문장 + “클라이언트의 관점에서” -> 상속

이름이 아니라 행동이 먼저

상속이 서브타이핑으로 사용될 경우에만 is-a 관계

리스코프 치환 원칙은 유연할 설계의 기반

리스코프 치환 원칙은 클라이언트가 어떤 자식 클래스와도 안정적으로 협력할 수 있는 상속 구조를 구현할 수 있는 가이드라인을 제공

새로운 자식 클래스를 추가하더라도 클라이언트의 입장에서 동일하게 행동하기만 한다면 클라이언트를 수정하기 않고도 상속 계층을 확장할 수 있다.

리스코프 치환 원칙을 따르는 설계는 유연할뿐만 아니라 확장성이 높다.

자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어짐

따라서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건

타입 계층과 리스코프 치환 원칙

클래스 상속은 타입 계층을 구현할 수 있는 다양한 방법 중 하나

핵심은 구현 방법과 무관하게 클라이언트의 관점에서 슈퍼타입에 대해 기대하는 모든 것이 서브타입에게도 적용돼야 한다는 것

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

클라이언트와 서버 사이의 협력을 의무와 이익으로 구성된 계약의 관점에서 표현하는 것 -> 계약에 의한 설계(Design By Contract, DBC)

계약에 의한 설계는 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 사전조건(precondition)과 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 사후조건(postcondition), 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 클래스 불변식(class invariant)의 세가지 요소로 구성

서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼타입 간에 체결된 계약을 준수해야 한다.

사전 조건 + 사후 조건도 만족해야 함

서브타입과 계약

자식 클래스가 부모 클래스의 서브타입이 되기 위해서는 다음 조건을 만족시켜야 함

서브타입에 더 강력한 사전조건을 정의할 수 없다.

서브타입에 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있다.

서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있다.

서브타입에 더 약한 사후조건을 정의할 수 없다.

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

따라서 서브타이핑을 위해 상속을 사용하고 있다면 부모 클래스가 클라이언트와 맺고 있는 계약에 관해 깊이 있게 고민하기 바란다.

상속은 타입 계층을 구현할 수 있는 전통적인 방법이지만 유일한 방법은 아니다.

또한 타입과 타입 계층을 구현하는 방법은 사용하는 프로그래밍 언어나 타입 체크의 시점에 따라 달라질 수 있다.

참조

  1. 오브젝트
This post is licensed under CC BY 4.0 by the author.