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

 

들어가며

 

흔히 객체지향 커뮤니티에서는 상속의 용도를 두가지로 바라보고 있다.

 

1. 타입 계층을 구현 (부모클래스는 자식클래스의 일반화이고 자식 클래스는 부모클래스의 특수화다.)

2. 코드 재사용

 

상속을 코드 재사용 목적으로 사용한다면 부모 클래스와 자식클래스를 강하게 결합시킨다.

따라서 상속은 코드 재사용이아니라 타입 계층을 구현하기위해 사용해야한다.

 

코드 재사용은 상속이 아닌 합성을 사용하는게 올바르다.

 

 

 

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

 

객체지향 프로그래밍 언어에서 타입을 구현하는 일반적인 방법은 클래스를 이용하는 것이다. 그리고 타입 계층을 구현하는 일반적인 방법은 상속을 이용하는것이다. 상속을 이용해 타입 계층을 구현한다는 것은 부모 클래스가 슈퍼타입의 역할을, 자식 클래스가 서브타입의 역할을 수행하도록 클래스 사이의 관계를 정의하는 것을 의미한다.

 

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

 


is - a 관계

 

마틴 오더스키의 조언에 따르면 두 클래스가 어휘적으로 is - a 관계를 모델링할 경우에만 상속을 사용해야 한다고 한다.

예를 면 어떤 타입 S가 다른 타입 T의 일종이라면 당연히 "타입 S는 타입 T다( S is-a T )" 라고 말할 수 있어야 한다.

하지만 새와 펭귄을 예로 들면 is - a 관계가 쉽게 배신할 수 있다는 사실을 알 수 있다.

 

 

- 펭귄은 새다.

- 새는 날 수 있다.

 

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

 


행동 호환성

 

타입의 이름 사이에 개념적으로 어떤 연광선이 있다고 하더라도 행동에 연관성이 없다면 is - a 관계를 사용하지 말아야 한다.

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

즉, 클라이언트 관점에서 두타입 모두 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층을 묶을 수 있지만 동일하지 않다고 기대한다면 묶어서는 안된다.

 

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

 

행동 호환성을 만족시키지 않는 상속 계층을 그대로 유지한 채 클라이언트의 기대를 충족시킬 수 있을까?

 

클라이언트의 기대에 따라 상속 계층을 분리

 

이로써 펭귄 타입의 인스턴스가 fly 메세지를 수신 할 일은 없을 것이다.

 

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

만약 Bird가 날 수 있으면서 걸을 수도 있어야 하고, Penguin은 오직 걸을 수만 있다고 가정하자.

 

인터페이스는 클라이언트가 기대하는 바에 따라 분리돼어야하므로

 

클라이언트 기대에 따른 인터페이스 분리

 

이제 Bird와 Penguin은 자신이 수행할 수 있는 인터페이스만 구현할 수 있다.

 

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

이러한 설계원칙을 인터페이스 분리 원칙(Interface Segregation Principle, ISP)라고 부른다.

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

 


서브클래싱과 서브타이핑

 

그래서 언제 상속을 사용해야하는가? 를 알기 위해서는 상속의 두 가지 목적의 특별한 이름인

서브클래싱, 서브타이핑을 살펴보자.

 

서브클래싱이란? 다른 캘르스의 코드를 재사용할 목적으로 상속을 사용하는 경우를 가리킴.

 

서브타이핑이란? 타입 계층을 구성하기 위해 상속을 사용하는 경우를 가리킴.

 

상속은 서브타이핑을 목적으로 사용해야 하므로 서브타이핑의 관계가 유지 되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야한다. 즉, 어떤 타입이 다른타입의 서브타입이 되기 위해서는 행동 호환성을 만족시켜야 한다.

그리고 행동 호환성을 만족하는 상속 관계는 부모 클래스를 새로운 자식 클래스로 대체하더라도 시스템이 문제없이 동작할 것이라는 것을 보장해야한다. 즉 대체 가능성을 포함한다.

 

지금까지 행동 호환성과 대체 가능성은 리스코프 치환원칙이라는 이름으로 정리되어 왔다.

 

 

 

04 리스코프 치환 원칙

 

is-a 관계 다시 살펴보기

 

오더스키가 설명한 is - a 관계는 행동이 호환되는 타입에 어떤 이름을 붙여야하 하는지 설명하는 가이드라고 생각하는 것이 좋다.

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

 

즉, 이름이 아니라 행동이 먼저다. 결론적으로 상속이 서브타이핑을 위해 사용될 경우에만 is - a 관계다.

 

 

 

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

 

서브 클래스와 서브타입은 서로 다른 개념이다. 어떤 클래스가 다른 클래스를 상속받으면 어떤 클래스는 그 다른 클래스의 서브클래스가 되지만 클라이언트의 관검에서 서브클래스가 슈퍼클래스를 대체할 수 없다면 서브타입이라고 할 수 없다.

 

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

 

즉, 계약에 의한 설계에 따르면 협력하는 클라이언트와 슈퍼타입의 인스턴스 사이에는 어떤 계약이 맺어져 있어 이 계약을 준수할 때만 정상적으로 협력할 수 있다.

 

이전 장들에서 살펴본 Movie와 DiscountPolicy 클래스를 통해 예를 들어 계약에 해당하는 사전조건과 사후조건을 알아보자.

 

 

 

DiscountPolicy의 calculateDiscountAmount 메서드는 인자로 전달된 screening의 null여부를 확인하지 않는다.

그러므로 screening을 받아 처리하기 이전 사전조건을 다음과 같이 표현할 수 있다.

 

assert acreening != null && screening.getStartTime().isAfter(LocalDateTime.now());

 

반환값 또한 null이 아니여야 하므로

사후조건은 다음과 같다.

 

assert amount != null && amount.isGreaterThanOrEqual(Money.ZERO);

 


서브타입과 계약

 

서브타입과  계약과의 관계는 다음과 같은 조건이 존재한다.

 

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

 

예를 들어 DiscountPolicy를 상속한 BrokenDiscountPolicy에 더 강력한 사전조건을 추가할 경우 Movie는 DiscountPolicy의 사전조건만 알고 있기 때문에 메세지 전송시 전혀 문제가 되지않는다고 가정할 것이다.

안타깝게도 BrokenDiscountPolicy의 사전조건은 이를 허용하지 않기 때문에 협력에 실패한다. 다시 말해 BrokenDiscountPolicy는 DiscountPolicy를 대체할 수 없기 때문에 서브타입이라고 할 수 없게된다.

 

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

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

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

 

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

 

 

 

'📚 Book > Object' 카테고리의 다른 글

15 디자인 패턴과 프레임워크  (0) 2020.05.10
14 일관성 있는 협력  (0) 2020.05.02
12 다형성  (0) 2020.04.12
11 합성과 유연한 설계  (0) 2020.03.29
10 상속과 코드 재사용  (0) 2020.03.22