오브젝트(조영호) 책 정리 -13-
상속의 용도
- 코드 재사용(서브 클래싱) : 코드를 재사용할 목적으로 상속을 사용하는 경우이다. 자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체 할 수 없다. 클래스 상속이라고 부른다.
- 타입 계층 구현(서브 타이핑) : 타입 계층을 구성하기 위해 상속을 사용하는 경우이다. 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체 할 수 있다. 부모 클래스는 자식 클래스의 슈퍼타입이고, 자식 클래스는 부모 클래스의 서브 타입이다. 인터페이스 상속이라고 부른다.
서브 클래싱과 서브 타이핑을 나누는 기준은 사용하는 목적이다. 자식 클래스가 부모 클래스의 코드를 재사용할 목적이었다면 서브 클래싱이고, 부모 클래스의 인스턴스 대신 자식 클래스의 인스턴스를 사용할 목적으로 상속을 했다면 서브 타이핑이다.
어떤 클래스가 다른 클래스를 상속받으면 그 클래스는 서브 클래스가 되지만 서브 타입인 것은 아니다. 클라이언트의 관점에서 자식 클래스가 부모 클래스를 대체 할 수 없다면 서브타입이라고 할 수 없다.
상속을 사용하는 목표는 코드 재사용이 아니라 타입 계층을 구현하는 것이다. 코드 재사용을 위해 상속을 사용한다면 부모 클래스와 자식 클래스를 강하게 결합시키기 때문에 설계의 변경에 방해가 된다. 반면 타입 계층을 목표로 상속을 사용하면 확장 가능하고 유연한 설계를 얻을 수 있다.
동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다. 상속은 타입 계층을 구현할 때 쉽고 편한 방법을 제공한다.
타입이란?
개념 관점의 타입
타입이란 우리가 인지하는 세상의 사물의 종류. 어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스라고 부른다. 또한 타입의 인스턴스를 객체라고 부른다.
타입의 구성
ex) 자바, 루비, 자바스크립트, C를 프로그래밍 언어라고 부를 때, 우리는 이들을 프로그래밍 타입으로 분류하는 것이다.
- 심볼 : 타입에 이름을 붙인 것. ex) '프로그래밍 언어'
- 내연 : 타입의 정의로서 타입에 속하는 객체들이 가지는 공통적인 속성이나 행동. ex) '컴퓨터에게 작업을 지시하기 위한 어휘'
- 외연 : 타입에 속하는 객체. ex) 자바, 루비, 자바스크립트, C
프로그래밍 언어 관점의 타입
연속적인 비트에 의미와 제약을 부여하기 위해 사용한다
타입의 목적
- 타입에 수행될 수 있는 유효한 오퍼레이션의 집합을 정의한다. ex) '+' 연산자
- 타입에 수행되는 오퍼레이션에 대해 미리 약속된 문맥을 정의한다 ex) 정수형에서 '+'는 덧셈이지만, 문자열은 문자열을 합치는 작업을 한다.
객체지향 패러다임 관점의 타입
객체의 타입이란 객체가 수신할 수 있는 메시지의 종류를 정의하는 것이다.
두 가지 관점
- 개념 관점에서 타입이란 공통의 특징을 공유하는 대상의 분류다
- 프로그래밍 언어 관점에서 타입이란 동일한 오퍼레이션을 적용할 수 있는 인스턴스의 집합이다.
즉, 동일한 오퍼레이션(메시지)를 수신할 수 있는 객체로 타입을 분류한다.
따라서 객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다. 동일한 퍼블릭 인터페이스를 제공하는 객체들은 동일한 타입으로 분류된다.
동일한 상태를 가지고 있더라도 퍼블릭 인터페이스가 다르다면 서로 다른 타입이고, 내부 상태가 다르더라도 동일한 퍼블릭 인터페이스를 공유한다면 동일한 타입이다.
객체를 바라볼 때 항상 객체가 외부에 제공하는 행동에 초점을 맞춰야 한다. 객체의 타입을 결정하는 것은 내부의 속성이 아니라 객체가 외부에 제공하는 행동이다.
타입의 관계
타입 계층을 구성하는 두 타입의 관계에서 더 일반적인 타입을 슈퍼 타입, 특수한 타입을 서브 타입이라고 부른다
슈퍼 타입의 특징
- 집합이 다른 집합의 모든 멤버를 포함한다
- 타입의 정의가 다른 타입보다 좀 더 일반적이다.
서브 타입의 특징
- 집합에 포함되는 인스턴스가 더 큰 집합에 포함된다
- 타입의 정의가 다른 타입보다 좀 더 구체적이다.
타입 계층 구현을 위해 언제 상속을 사용해야 할까?
아래의 질문에 모두 '예'라고 답할 수 있는 경우에만 상속을 사용하는 것이 바람직하다.
1. 상속 관계가 is-a 관계를 모델링하는가?
객체 지향에서 우리는 객체가 수행하는 행동에 따라 타입을 결정하였다. 따라서 타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다. 타입이 개념적으로 어떤 연관성이 있다고 하더라도 행동에 연관성이 없다면 is-a 관계를 사용하지 말아야 한다.
ex) 펭귄은 새이다.
일반적인 사실로 묶는다면 펭귄은 새라는 타입에 포함될 것이다. 하지만 클라이언트가 '새는 날 수 있다' 라는 행동을 기대한다면 펭귄은 새라는 타입에 들어갈 수 없다.
만약 클라이언트의 요구하는 행동의 특성과 개념적인 특성을 모두 만족하고 싶다면 우리는 클라이언트의 기대에 따라 상속 계층을 분리 할 수 있다.
클라이언트에 따라 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있으며 도메인의 특성과 행동의 특성을 모두 만족시킬 수 있다. *인터페이스 분리 원칙
*인터페이스 분리 원칙 : 인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙
주의할 점 : 위처럼 설계를 하는 경우는 날 수 있는 새와 펭귄의 구분이 필요할 때의 이야기이다. 설계가 꼭 현실 세계를 반영할 필요가 없다. 현재의 요구사항이 '날 수 없는 행동'에 관심이 없다면 FlyingBird를 추가하는 것은 설계를 불필요한 복잡함이다. 중요한 것은 설계가 반영할 도메인의 요구사항이고 그 안에 클라이언트가 객체에게 요구하는 행동이다,
2. 클라이언트 입장에서 부모 클래스의 타입으로 자식 클래스 사용해도 무방한가?
자식 클래스가 부모 클래스를 대신할 수 있기 위해서는 자식 클래스가 부모 클래스가 사용되는 모든 문맥에서 자식 클래스와 동일하게 행동할 수 있어야 한다. 또한 상속 관계에서 부모 클래스를 새로운 자식 클래스로 대체하더라도 시스템이 문제없이 동작하는 것을 보장해야 한다. *리스코프 치환 원칙(LSP)
*리스코프 치환 원칙 : 클라이언트가 "차이점을 인식하지 못한 채 파생 클래스의 인터페이스를 통해 서브 클래스를 사용 할 수 있어야 한다"
리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트의 가정을 준수해야 한다는 것을 말한다. 중요한 것은 클라이언트 관점에서 자식 클래스와 부모 클래스의 행동이 호횐되는지 여부이다.
*계약에 의한 설계를 사용하면 리스코프 치환 원칙이 강제하는 조건을 좀 더 명확하게 찾을 수 있다,
계약의 의한 설계
사전조건 : 클라이언트가 정상적으로 메서드를 실행하기 위해 만족시켜야 하는 조건
사후조건 : 메서드가 실행된 후에 서버가 클라이언트에게 보장해야 하는 조건
클래스 불변식 : 메서드 실행 전과 실행 후에 인스턴스가 만족시켜야 하는 규칙(객체 내부 상태와 관련)
서브 타이핑을 만족하는 사전조건
서브타입에 더 강력한 사전 조건을 정의할 수 없다
서브타입에 슈퍼타입과 같거나 더 약한 사전조건을 정의할 수 있다.
서브 타이핑을 만족하는 사후조건
서브타입에 슈퍼타입과 같거나 더 강한 사후조건을 정의할 수 있다
서브타입에 더 약한 사후조건을 정의할 수 없다
클라이언트는 슈퍼타입의 사전조건과 사후조건만을 알고 있다. 슈퍼 타입의 사전조건은 서브 타입에서 최소한으로 보장해야 하는 사전조건이다. 또한 슈퍼 타입의 사후조건은 서브 타입에서 최대한으로 보장할 수 있는 사후조건이다.