이 글은 오브젝트라는 책을 읽고 정리한 글입니다.
13장에서는 슈퍼타입과 서브타입의 개념을 설명하고 타입 계층을 만족시키기 위해 적용할 수 있는 설계 원칙을 설명합니다.
상속의 첫 번째 용도는 타입 계층을 구현하는 것이다. 타입 계층의 관점에서 부모 클래스는 자식 클래스의 일반화(generalization)이고 자식 클래스는 부모 클래스의 특수화(specialization)다.
상속의 두 번째 용도는 코드 재사용이다. 하지만 재사용을 위해 상속을 사용할 경우 부모 클래스와 자식 클래스가 강하게 결합되기 때문에 변경하기 어려운 코드를 얻게 될 확률이 높다.
상속을 사용하는 일차적인 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이어야 한다. 반면 타입 계층을 목표로 상속을 사용하면 다형적으로 동작하는 객체들의 관계에 기반해 확장 가능하고 유연한 설계를 얻을 수 있게 된다.
결론부터 말하자면 동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다.
타입
개념 관점의 타입
개념 관점에서 타입이란 우리가 인지하는 세상의 사물의 종류를 의미한다.
어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스라고 부른다. 일반적으로 타입의 인스턴스를 객체라고 부른다.
3장 타입과 추상화
이 글은 객체지향의 사실과 오해라는 책을 읽고 정리한 글입니다. 3장에서는 소프트웨어 개발에서 가장 중요한 개념인 추상화와 마주치게 될 것 입니다. 일단 컴퓨터를 조작하는 것이 추상화를
shyunk223.tistory.com
- 심볼 : 타입에 이름을 붙인 것.
- 내연(intension) : 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동
- 외연(extension) : 타입에 속하는 객체들의 집합
프로그래밍 언어 관점의 타입
프로그래밍 언어 관점에서 타입은 연속적인 비트에 의미와 제약을 부여하기 위해 사용된다. 프로그래밍 언어의 관점에서 타입은 비트 묶음에 의미를 부여하기 위해 정의된 제약과 규칙을 가리킨다.
프로그래밍 언어에서 타입은 두 가지 목적을 위해 사용됩니다.
- 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의한다.
- 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 제공한다.
적용 가능한 오퍼레이션의 종류와 의미를 정의함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용된다.
객체지향 패러다임 관점의 타입
타입을 다음과 같은 두가지 관점에서 정의할 수 있다.
- 개념 관점에서 타입이란 공통의 특징을 공유하는 대상들의 분류다.
- 프로그래밍 언어 관점에서 타입이란 동일한 오퍼레이션을 적용할 수 있는 인스턴스들의 집합이다.
객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.
객체지향 프로그래밍에서 오퍼레이션은 객체가 수신할 수 있는 메시지를 의미한다. 따라서 객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.
객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 따라서 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.
객체에게 중요한 것은 속성이 아니라 행동이라는 사실이다.
타입계층
타입 사이의 포함관계
타입 계층을 구성하는 두 타입 간의 관계에서 더 일반적인 타입을 슈퍼타입(supertype)이라 부르고 더 특수한 타입을 서브타입(subtype)이라고 부른다.
- 일반화 : 다른 타입을 완전히 포함하거나 내포하는 타입을 식별하는 행위 또는 그 행위의 결과
- 특수화 : 다른 타입 안에 전체적으로 포함되거나 완전히 내포되는 타입을 식별하는 행위 또는 그 행위의 결과
내연과 외연의 관점에서 서브타입과 슈퍼타입을 다음과 같이 정의할 수 있다.
- 슈퍼타입
- 집합이 다른 집합의 모든 멤버를 포함한다
- 타입 정의가 다른 타입보다 좀 더 일반적이다
- 서브타입
- 집합에 포함되는 인스턴스들이 더 큰 집합에 포함된다
- 타입 정의가 다른 타입보다 좀 더 구체적이다
객체지향 프로그래밍과 타입 계층
퍼블릭 인터페이스의 관점에서 슈퍼타입과 서브타입을 다음과 같이 정의할 수 있다.
- 슈퍼타입 : 서브타입이 정의한 퍼블릭 인터페이스를 일반화시켜 상대적으로 범용적이고 넓은 의미로 정의한 것
- 서브타입 : 슈퍼 타입이 정의한 퍼블릭 인터페이스를 특수화시켜 상대적으로 구체적이고 좁은 의미로 정의한 것.
서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.
서브 클래싱과 서브타이핑
객체지향 프로그래밍 언어에서 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다. 그리고 타입 계층을 구현하는 일반적인 방법은 상속을 이용하는 것이다. 상속을 이용해 타입 계층을 구현한다는 것은 부모 클래스가 슈퍼타입의 역할을, 자식 클래스가 서브타입의 역할을 수행하도록 클래스 사이의 관계를 정의한다는 것을 의미한다.
언제 상속을 사용해야 하는가?
마틴 오더스키는 다음과 같은 두 질문을 해보고 모두 '예'라고 말할 수 있을 경우에만 상속을 사용하라고 조언한다.
- 상속 관계가 is-a 관계를 모델링하는가?
- 일반적으로 "[자식 클래스]는 [부모 클래스]다"라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주할 수 있다.
- 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스를 사용해도 무방한가?
- 상속 계층을 사용하는 클라이언트의 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 한다. 이를 자식 클래스와 부모 클래스 사이의 행동 호환성이라고 부른다.
is-a 관계
먼저 익숙한 두 가지 사실에서 이야기를 시작해보자.
- 펭귄은 새다
- 새는 날 수 있다
펭귄은 분명 새지만 날 수 없는 새다. 이 예는 어휘적인 정의가 아니라 기대되는 행동에 따라 타입 계층을 구성해야 한다는 사실을 보여준다.
타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다. 따라서 슈퍼타입과 서브타입 관계에서는 is-a보다 행동 호환성이 더 중요하다.
행동 호환성
여기서 중요한 것은 행동 호환성을 판단하는 기준은 클라이언트의 관점이라는 것이다. 클라이언트가 두 타입이 동일하게 행동할 것이라고 기대하면 두 타입을 타입 계층으로 묶을 수 있다.
public void flyBird(Bird bird) {
// 인자로 전달된 모든 bird는 날 수 있어야 한다.
bird.fly();
}
Penguin은 날 수 없고 클라이언트는 모든 bird가 날 수 있기를 기대하기 때문에 flyBird 메서드로 전달돼서는 안 된다. Penguin은 클라이언트의 기대를 저버리기 때문에 Bird의 서브 타입이 아니다. 따라서 아 둘을 상속 관계로 연결한 위 설계는 수정돼야 한다.
클라이언트의 기대에 따라 계층 분리하기
문제를 해결할 수 있는 방법은 클라이언트의 기대에 맞게 상속 계층을 분리하는 것뿐이다. 따라서 날 수 있는 새와 날 수 없는 새를 명확하게 구분할 수 있게 상속 계층을 분리하면 서로 다른 요구사항을 가진 클라이언트를 만족시킬 수 있을 것이다.
public class Bird {
...
}
public class FlyingBird extends Bird {
public void fly() { ... }
...
}
public class Penguin extends Bird {
...
}
클라이언트에 따라 인터페이스를 분리하면 각 클라이언트의 요구가 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있게 된다.
이처럼 인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙(Integerface Segrefation Principle, ISP)이라고 부른다.
요점은 자연어에 현혹되지 말고 요구사항 속에서 클라이언트가 기대하는 행동에 집중하라는 것이다. 두 클래스 사이에 행동이 호환되지 않는다면 올바른 타입 계층이 아니기 때문에 상속을 사용해서는 안된다.
서브클래싱과 서브타이핑
- 서브클래싱(subclassing)
- 다른 클래스의 코드를 재사용할 목적으로 상속을 사용하는 경우
- 구현 상속 또는 클래스 상속이라고 부르기도 한다.
- 서브타이핑(subtyping)
- 타입 계층을 구성하기 위해 상속을 사용하는 경우
- 인터페이스 상속이라고 부르기도 한다.
서브클래싱과 서브타이핑을 나누는 기준은 상속을 사용하는 목적이다.
서브 타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼 타입이 하는 모든 행동을 동일하게 할 수 있어야 한다. 어떤 타입이 다른 타입의 서브타입이 되기 위해서는 행동 호환성을 만족시켜야 한다.
행동 호환성을 만족하는 상속 관계는 부모 클래스를 새로운 자식 클래스로 대체 하더라도 시스템이 문제 없이 동작할 것이라는 것을 보장해야 한다. 다시 말해서 자식 클래스와 부모 클래스 사이의 행동 호환성은 부모 클래스에 대한 자식 클래스의 대체 가능성을 포함한다.
행동 호환성과 대체 가능성은 올바른 상속 관계를 구축하기 위해 따라야 할 지침이라고 할 수 있다.
리스코프 치환 원칙
리스코프 치완 원칙은 "서브 타입은 그것의 기반 타입에 대해 대체 가능해야 한다"는 것으로 클라이언트가 "차이점을 인식하지 못한 채 파생 클래스의 인터페이스를 통해 서브 클래스를 사용할 수 있어야 한다"는 것이다. 리스코프 치환 원칙에 따르면 자식 클래스가 부모 클래스와 행동 호환성을 유지함으로써 부모 클래스를 대체할 수 있도록 구현된 상속 관계만을 서브 타이핑이라고 불러야 한다.
클라이언트와 대체 가능성
리스코프 치환 원칙은 상속 관계에 있는 두 클래스 사이의 관계를 클라이언트와 떨어트려 놓고 판단하지 말라고 속삭인다. 상속 관계는 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체할 수 있을 때만 올바르다.
대체 가능성을 결정하는 것은 클라이언트다.
is-a 관계 다시 살펴보기
객체지향과 관련된 대부분의 규칙이 그런 것처럼 is-a 관계 역시 행동이 우선이다.
상속이 서브 타이핑을 위해 사용될 경우에만 is-a관계다. 서브 클래싱을 구현하기 위해 상속을 사용했다면 is-a 관계라고 말할 수 없다.
리스코프 치환 원칙은 유연한 설계의 기반이다.
자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어진다. 따라서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다. 일반적으로 리스코프 치환 원칙 위반은 잠재적인 개방-폐쇄 원칙 위반이다.
계약에 의한 설계와 서브타이핑
클라이언트와 서버 사이의 협력을 의무(obligation)와 이익(benefit)으로 구성된 계약의 관점에서 표현하는 것을 계약에 의한 설계라고 부른다. 계약에 의한 설계는 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 사전 조건(precondition)과 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 사후 조건(postcondirion)과 메서드 실행 전과 실행후에 인스턴스가 만족시켜야 하는 클래스 불변식(class invariant)의 세 가지 요소로 구성된다.
서브타입이 리스코프 치환 원칙을 만족시키기 위해서는 클라이언트와 슈퍼 타입 간에 체결된 계약을 준수해야 한다.
서브타입과 계약
- 서브타입에 더 강력한 사전 조건을 정의할 수 없다.
- 서브타입에 더 약한 사후 조건을 정의할 수 없다
- 서브타입에 슈퍼 타입과 같거나 더 약한 사전조건을 정의할 수 있다.
- 서브타입에 슈퍼타입과 같거나 더 강한 사후 조건을 정의할 수 있다.
'📖 > 오브젝트' 카테고리의 다른 글
[오브젝트] 15장 디자인 패턴과 프레임워크 (0) | 2024.03.19 |
---|---|
[오브젝트] 14장 일관성 있는 협력 (0) | 2024.03.14 |
[오브젝트] 12장 다형성 (1) | 2024.03.07 |
[오브젝트]11장 합성과 유연한 설계 (0) | 2024.03.04 |
[오브젝트] 10장 상속과 코드 재사용 (0) | 2024.03.04 |