본문 바로가기
스터디/오브젝트

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

by 검은도자기 2023. 10. 16.

개요

이번 장에서는 상속을 예시로 올바른 타입 계층을 구성하는 원칙을 좀 더 깊이 있게 소개합니다.

 

 

기존 상속에 관한 오해

올바른 타입 계층을 구성하는 원칙을 이해하기 위해 먼저 상속은 다음 두 가지 용도로 사용된다는 이해가 필요합니다. 

 

타입 계층 구현

타입 계층 안에서 부모 클래스는 일반적인 개념(일반화), 자식 클래스는 특수한 개념(특수화)을 구현합니다.

 

코드 재사용

점진적으로 앱의 기능을 확장할 수 있습니다

 

재사용 목적으로 상속을 사용할 경우 부모 클래스와 자식 클래스가 강하게 결합되기에 변경하기 어려운 코드를 얻게 될 확률이 높습니다.

 

 

타입 계층이란 무엇이고 상속을 이용해 타입 계층을 구현한다는 것이 무엇을 의미할까?

이 질문의 답을 찾기 위해 먼저 타입과 타입 계층의 개념을 알아보겠습니다.

 

 

타입

객체지향에서 타입의 의미를 이해하기 위해 먼저 프로그래밍 언어 관점에서의 타입과 개념 관점에서의 타입을 함께 살펴봐야 합니다.

 

개념 관점의 타입

개념 관점에서 타입은 우리가 인식하는 세상의 사물(객체들에 적용하는 개념이나 아이디어)의 종류입니다.

타입은 사물을 분류하기 위한 틀로 사용되며, 어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스(instance) 혹은 객체라고 부릅니다

 

타입은 다음 세 가지 요소로 구성이 됩니다.

 

심볼(symbol)

타입에 이름을 붙인 것입니다.

 

내연(intension)

타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동을 가리키며, 일반적으로 타입에 속하는 객체들이 공유하는 속성과 행동의 집합이 내연을 구성합니다.

 

외연(extension)

타입에 속하는 객체들의 집합입니다.

 

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

프로그래밍 언어 관점에서 타입은 비트 묶음에 의미를 부여하기 위해 정의된 제약과 규칙을 가리킵니다.

 

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

 

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

정리하자면 프로그래밍 언어 관점에서 타입은 적용 가능한 오퍼레이션의 종류와 의미를 정의함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용됩니다.

 

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

타입을 다음과 같은 두 가지 관점에서 정의할 수 있습니다.

 

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

위 두 타입의 정의를 객체지향 패러다임의 관점에서 조합해 보면 다음과 같습니다.

프로그래밍 언어의 관점에서 타입은 호출 가능한 오퍼레이션의 집합을 정의하며, 객체지향에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미합니다.

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

 

객체지향에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 같습니다.

그렇기에 객체지향 관점에서 타입을 다음과 같이 정의할 수 있습니다.

 

객체의 퍼블릭 인터페이스는 그 객체의 타입을 결정하며, 동일한 퍼블릭 인터페이스를 가진 객체들은 같은 타입으로 분류됩니다.

따라서 객체지향 관점의 타입 의미를 봤을 때 객체에게 중요한 것은 속성이 아닌 행동이라는 사실을 강조합니다.

 

 

타입 계층

 

타입 사이의 포함관계

타입 간의 포함관계를 집합의 관점에서 이해할 수 있습니다.

수학에서 집합은 다른 집합을 포함할 수 있습니다 그렇기에 타입 역시 객체들의 집합이기 때문에 다른 타입을 포함하는 것이 가능합니다.

타입 안에 포함된 객체들을 좀 더 상세한 기준으로 묶어 새로운 타입을 정의하면 이 새로운 타입은 자연스럽게 기존 타입의 부분집합이 됩니다.

더 세분화된 타입의 집합이 다른 타입의 부분집합으로 포함될 수 있기 때문에, 동일한 인스턴스가 여러 타입으로 분류될 수도 있습니다.

 

포함하는 타입은 인스턴스의 수가 더 많으며, 외연(범위) 관점에서는 더 크고 내연(내용) 관점에서는 더 일반적입니다.

반면, 포함되는 타입은 외연 관점에서는 더 작고 내연 관점에서는 더 특수합니다.

이것은 포함 관계로 연결된 타입 사이에 개념적으로 일반화와 특수화의 관계가 존재한다는 것을 의미합니다.

 

타입들은 다음 사진과 같이 일반화와 특수화 관계를 가진 계층으로 표현할 수 있습니다.

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

위 사진에서는 프로그래밍 언어 타입은 객체지향 언어 타입과 절차적 언어 타입의 슈퍼타입이고, 객체지향 언어 타입은 클래스 기반 언어 타입과 프로토타입 기반 언어 타입의 슈퍼타입입니다.

 

내연과 외연의 관점에서 일반화와 특수화를 정의해 보겠습니다.

객체의 정의를 의미하는 내연 관점

 

  • 일반화란 어떤 타입의 정의를 좀 더 보편적이고 추상적으로 만드는 과정을 의미합니다.
  • 특수화란 어떤 타입의 정의를 좀 더 구체적이고 문맥 종속적으로 만드는 과정을 의미합니다.
  • 내연의 관점에서 특수한 타입의 정의는 일반적인 타입의 정의를 좀 더 구체화한 것입니다.

 

집합을 의미하는 외연의 관점

 

  • 일반적인 타입의 인스턴스 집합은 특수한 타입의 인스턴스 집합을 포함하는 슈퍼셋입니다.
  • 특수한 타입의 인스턴스 집합은 일반적인 타입의 인스턴스 집합에 포함된 서브셋입니다.
  • 따라서 특수한 타입에 속한 인스턴스는 동시에 더 일반적인 타입의 인스턴스이기도 합니다.

 

위 관점들을 토대로 일반화와 특수화를 다음과 같이 정의할 수 있습니다.

 

  • 일반화는 다른 타입을 완전히 포함하거나 내포하는 타입을 식별하는 행위 또는 그 행위의 결과를 가리킵니다.
  • 특수화는 다른 타입 안에 전체적으로 포함되거나 완전히 내포되는 타입을 식별하는 행위 또는 그 행위의 결과를 가리킵니다.

 

내연의 관점에서 서브타입의 정의가 슈퍼타입의 정의보다 더 구체적이고 외연의 관점에서 서브타입에 속하는 객체들의 집합이 슈퍼타입에 속하는 객체들의 집합에 포함된다는 사실을 알 수 있습니다.

 

따라서 내연과 외연의 관점에서 서브타입과 슈퍼타입을 다음과 같이 정의할 수 있습니다.

슈퍼타입의 특징을 가지는 타입

 

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

 

서브타입의 특징을 가지는 타입

 

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

 

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

객체의 타입을 결정하는 것은 퍼블릭 인터페이스이기에 일반화와 특수화를 다음과 같이 정의할 수 있습니다.

일반적인 타입

비교하려는 타입에 속한 객체들의 인터페이스보다 더 일반적인 인터페이스를 가지는 객체들의 타입을 의미합니다.

 

특수한 타입

비교하려는 타입에 속한 객체들의 인터페이스보다 더 특수한 인터페이스를 가지는 객체들의 타입을 의미합니다.

 

따라서 퍼블릭 인터페이스의 관점에서 슈퍼타입과 서브타입을 다음과 같이 정의할 수 있습니다.

슈퍼타입

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

 

서브타입

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

 

그래서 상속을 이용해 타입 계층을 구현한다는 것은 무슨 의미인 걸까?

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

 

 

언제 상속을 사용해야 할까?

이 책에서는 다음 질문을 해보고 두 질문에 모두 예라고 답할 수 있는 경우에만 상속을 사용하라고 조언합니다.

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

 

  • 이것은 앱을 구성하는 어휘에 대한 우리의 관점에 기반합니다.
  • 일반적으로 자식 클래스는 부모 클래스다라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주할 수 있습니다.

 

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

 

  • 상속 계층을 사용하는 클라이언트의 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 합니다.
  • 이를 자식 클래스와 부모 클래스 사이의 행동 호환성이라고 부릅니다.

 

설계 관점에서 상속을 적용할지 여부를 결정하기 위해 첫 번째 질문보다는 두 번째 질문에 초점을 맞추는 것이 중요합니다.

클라이언트의 관점에서 두 클래스에 대해 기대하는 행동이 다르다면 비록 그것이 어휘적으로 is-a 관계로 표현할 수 있다고 하더라도 상속을 사용해서는 안됩니다.

 

어휘적으로 is-a 관계로 표현할 수 있다고 하더라도 상속을 사용해서는 안 되는 걸까?

"객체지향 언어는 프로그래밍 언어다"라고 표현할 수 있고 "클래스 기반 언어는 객체지향 언어다"라고 표현할 수 있기 때문에 프로그래밍 언어, 객체지향 언어, 클래스 기반 언어는 is-a 관계를 만족시킵니다.

 

하지만 is-a 관계가 생각처럼 직관적이고 명쾌한 것은 아닙니다.

왜냐하면 타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있기 때문입니다.

 

그렇기에 타입의 이름 사이에 개념적으로 어떤 연관성이 있다고 하더라도 행동에 연관성이 없다면 is-a 관계를 사용하지 말아야 하며, 두 타입 사이에 행동이 호환될 경우에만 타입 계층으로 묶어야 합니다.

 

 

행동이 호환된다는 것은 무슨 의미일까?

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

 

  • 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있습니다.
  • 클라이언트가 두 타입이 동일하게 행동하지 않을 것이라고 기대한다면 두 타입을 타입 계층으로 묶어서는 안 됩니다.
  • 타입 계층을 이해하기 위해서는 그 타입 계층이 사용될 문맥을 이해하는 것이 중요합니다.

 

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

행동 호환성을 만족시키지 않는 상속 계층을 그대로 유지한 채 클라이언트의 기대를 충족시킬 수 있는 방법은 클라이언트의 기대에 맞게 상속 계층을 분리하는 것뿐입니다.

 

만약 Penguin이 Bird의 코드를 재사용해야 한다면 어떻게 해야 할까?

Bird의 인터페이스를 통해 재사용 가능하다는 전제를 만족시키는 전제하에 합성을 재사용하는 방법이 좋은 방법입니다.

 

클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있게 됩니다.

 

위 예시처럼 인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙(ISP)이라고 부릅니다.

 

중요한 것은 설계가 반영할 도메인의 요구사항이고 그 안에서 클라이언트가 객체에게 요구하는 행동입니다.

따라서 현실을 정확하게 묘사하는 것이 아니라 요구사항을 실용적으로 수용하는 것을 목표로 삼아야 합니다.

그렇기에 두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안 됩니다.

 

서브클래싱과 서브타이핑

사람들은 상속을 사용하는 두 가지 목적에 특별한 이름을 붙였는데 서브클래싱과 서브타이핑이 그것입니다.

서브클래싱(subclassing)

 

  • 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우

 

서브타이핑(subtyping)

 

  • 타입 계층을 구성(부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용) 하기 위해 상속을 사용하는 경우
  • 서브타이핑 관계를 유지하려면, 서브타입은 슈퍼타입이 수행하는 모든 행동을 동일하게 수행할 수 있어야 합니다.

 

어떤 타입이 다른 타입의 서브타입이 되려면, 행동 호환성을 만족시켜야 하며, 자식 클래스와 부모 클래스 사이의 행동 호환성은 자식 클래스의 부모 클래스에 대한 대체 가능성을 포함합니다.
행동 호환성과 대체 가능성은 올바른 상속 관계를 구축하기 위한 지침이라 할 수 있으며, 이 지침은 리스코프 치환 원칙이라는 이름으로 정리되어 소개되었습니다.

 

 

리스코프 치환 원칙

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

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

 

클라이언트와 대체 가능성

리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체하려면, 부모 클래스에 대한 클라이언트의 가정을 준수해야 함을 강조합니다.

이 원칙은 "클라이언트와 격리한 상태에서 이 모델을 유효하게 검증할 수 없다"는 중요한 결론을 도출합니다.

이 결론으로 알 수 있는 것은 모델의 유효성은 오직 클라이언트의 관점에서만 검증될 수 있다는 의미입니다.

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

 

is-a 관계 다시 살펴보기

클라이언트 관점에서 자식 클래스의 행동이 부모 클래스의 행동과 호환되지 않고 그로 인해 대체가 불가능하다면 어휘적으로 is-a라고 말할 수 있다고 하더라도 그 관계를 is-a 관계라고 할 수 없습니다.

일반적으로 클라이언트를 고려하지 않은 채 개념과 속성의 측면에서 상속 관계를 정할 경우 리스코프 치환 원칙을 위반하는 서브클래싱에 이르게 될 확률이 높습니다.

슈퍼타입과 서브타입이 클라이언트 입장에서 행동이 호환된다면 두 타입을 is-a로 연결해 문장을 만들어도 어색하지 않은 단어로 타입의 이름을 정하라는 것입니다.

상속이 서브타이핑을 위해 사용될 경우에만 is-a 관계며, 서브클래싱을 구현하기 위해 상속을 사용했다면 is-a 관계라고 말할 수 없습니다.

 

 

클라이언트 관점에서 자식 클래스가 부모 클래스를 대체수 있다는 것은 무엇을 의미하는 걸까?

클라이언트와 서버 사이의 협력을 의무(Obligation)와 이익(Benefit)으로 구성된 계약의 관점에서 표현하는 것을 계약에 의한 설계라고 합니다.

 

계약에 의한 설계는 다음의 세 가지 요소로 구성되어 있습니다.

사전조건(Percondition)

클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 것

 

사후조건(Postcondition)

메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 것

 

클래스 불변식(Class invariant)

메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 것

 

리스코프 치환 원칙에서 따라서 계약에 의한 설계를 사용하면 리스코프 치환 원칙이 강제하는 조건을 계약의 개념을 이용해 좀 더 명확하게 설명할 수 있습니다.

계약에 의한 설계

협력하는 클라이언트와 슈퍼타입의 인스턴스 사이에는 어떤 계약이 맺어져 있는데 클라이언트와 슈퍼타입은 이 계약을 준수할 때만 정상적으로 협력할 수 있습니다.

 

리스코프 치환 원칙

서브타입이 그것의 슈퍼타입을 대체할 수 있어야 하고 클라이언트가 차이점을 인식하지 못한 채 슈퍼타입의 인터페이스를 이용해 서브타입과 협력할 수 있어야 한다고 말합니다.

 

클라이언트의 입장에서 서브타입은 정말 슈퍼타입의 '한 종류'여야 하는 것입니다.

따라서 서브타입이 슈퍼타입처럼 보일 수 있는 유일한 방법은 클라이언트가 슈퍼타입과 맺은 계약을 서브타입이 준수하는 것뿐입니다.

 

계약의 관점에서 상속이 초래하는 가장 큰 문제

계약의 관점에서 상속이 초래하는 가장 큰 문제는 자식 클래스가 부모 클래스의 메서드를 오버라이딩할 수 있다는 것입니다.

그렇기에 자식 클래스가 부모 클래스의 서브타입이 되기 위해서는 다음 조건들을 만족시켜야 합니다.

 

  • 서브타입에 더 강력한 사전조건을 정의할 수 없습니다.
  • 서브타입에 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있습니다.
  • 서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있습니다.
  • 서브타입에 더 약한 사후조건을 정의할 수 없습니다.

 

리스코프 치환 원칙을 설명하기 위해 계약에 의한 설계 개념을 이용할 수 있습니다.

 

  • 어떤 타입이 슈퍼타입에서 정의한 사전조건보다 더 약한 사전조건을 정의하고 있다면 그 타입은 서브타입이 될 수 있지만 더 강한 사전조건을 정의한다면 서브타입이 될 수 없습니다.
  • 어떤 타입이 슈퍼타입에서 정의한 사후조건보다 더 강한 사후조건을 정의하더라도 그 타입은 여전히 서브타입이지만 더 약한 사후조건을 정의한다면 서브타입의 조건이 깨지고 맙니다.

 

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

.

 

마무리

오늘은 오브젝트 13장을 스터디하고 이해한 내용들을 정리해 보았습니다.

이번 장은 처음 읽었을 때 이해가 잘 되지 않아서 여러번 읽어서 어느 정도 이해가 되었네요..

스터디 전에는 상속은 이럴 때 사용해야한다 정도로만 이해를 했었습니다.

스터디 후에는 상속을 사용해야 하는 이유 중 하나인 타입 계층에 대한 이해 그리고 타입 계층을 어떤 고민을 하면서 설계해야할지 좀 더 깊게 이해한거 같아서 좋았습니다.

이번 포스팅은 마무리하면서 다음 포스팅에서 뵙겠습니다.

 

 

참고

http://www.yes24.com/Product/Goods/74219491

 

오브젝트 - 예스24

역할, 책임, 협력을 향해 객체지향적으로 프로그래밍하라!객체지향으로 향하는 첫걸음은 클래스가 아니라 객체를 바라보는 것에서부터 시작한다. 객체지향으로 향하는 두번째 걸음은 객체를

www.yes24.com

 

'스터디 > 오브젝트' 카테고리의 다른 글

15장 - 디자인 패턴과 프레임워크  (0) 2023.11.14
14장 - 일관성 있는 협력  (1) 2023.10.26
12장 - 다형성  (0) 2023.09.20
11장 - 합성과 유연한 설계  (0) 2023.08.28
10장 - 상속과 코드 재사용  (0) 2023.08.06