Ch13 서브클래싱과 서브타이핑
들어가며
13장 서브클래싱과 서브타이핑 은 부록 계약에 의한 설계 와 함께 요약한다.
핵심 내용은 클라이언트의 관점
에서 행동 호환성
이 만족될 때 서브타이핑
의 관계가 성립한다는 것이다.
그리고 행동 호환성
을 만족시킨다는 의미가 무엇인지 리스코프 치환 원칙
과 계약에 의한 설계
를 통해 설명한다.
상속은 코드 재사용을 목적으로 사용하면 안된다.
코드 재사용
을 위해 사용된 상속
을 서브 클래싱
이라고 한다.
서브클래싱
은 컴파일 타임에 부모 클래스와 자식 클래스가 정적으로 결합 하고, 이런 특성 때문에 런타임에 동적으로 관계 변경이 불가능하다.
만일 코드 재사용
을 목적으로 상속을 사용하게 되면, 취약한 기반 클래스
문제가 발생할 가능성이 높아지는데, 이는 설계의 진화와 변경을 방해한다.
- 취약한 기반 클래스 문제
- 부모 클래스의
내부 구현 변경
에 자식 클래스까지 영향을 받는다.- 부모 클래스 내부 메서드 호출 순서와 같은
구체적인 내부 구현 사항
변경에 부모 클래스의 코드를 재사용 하는 자식 클래스 까지 파급 효과가 미친다. - 자식 클래스가 부모 클래스의 변경에 발맞춰 함께 변경되어야 하는 상황이 생긴다.
- 부모 클래스 내부 메서드 호출 순서와 같은
- 부모 클래스의 개발자가 세운
가정
을 정확히 파악하고 부모 클래스의 내부 구현에 대해 알아야 한다.- 부모 클래스의 가정을 정확히 파악하고 자식 클래스를 개발해야 하기 때문에 캡슐화가 저해된다.
- 부모 클래스 내부 구현 상 메서드 호출 순서가 존재하거나, 메서드 호출에 따른 상태 변경 등의 상세 구현 내용을 정확히 파악해야 한다.
- 부모 클래스의
상속을 사용하는 목적은 타입 계층의 구현이어야 한다.
타입 계층의 구현
을 위한 상속을 서브 타이핑
이라고 한다. 타입 계층
을 목표로 상속을 사용하면 다형적인 협력이 가능한 객체들의 관계 를 얻음으로써 유연한 설계를 구현할 수 있다.
타입이란
- `타입`이란 **공통의 특징을 공유하는 대상들의 분류**
- `타입`이란 **동일한 오퍼레이션을 적용할 수 있는 객체들의 집합**
위 정의에서 알 수 있듯이 타입
을 정의하는 가장 중요한 키워드는 바로 오퍼레이션
이다.
타입
은 호출 가능한 오퍼레이션의 집합 에 의해 정의된다. 쉽게 말하자면, 어떤 객체가 특정 타입이기 위해서는 해당 타입의 오퍼레이션을 수행할 수 있어야 한다.
객체지향 프로그래밍
에서 타입
을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일 하다.
- **객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다.**
- **동일한 퍼블릭 인터페이스를 갖는 객체는 동일한 타입으로 간주할 수 있다.**
올바른 상속, 올바른 타입 계층
올바른 상속 이란 타입 계층의 구현
을 위한 상속을 의미한다. 그렇다면 타입 계층
을 구현한다는 의미는 무엇일까?
클라이언트 입장
에서슈퍼 타입
과서브 타입
의 차이점을 모르고 사용할 수 있어야 한다.- 즉
클라이언트의 관점
에서 두 클래스에기대하는 행동
이 같아야 한다. 슈퍼 타입
과서브 타입
간에행동 호환성
이 만족되어야타입 계층
을 위한상속
이라고 할 수 있다.
- 즉
행동 호환성의 의미
행동 호환성
이 만족되어야만 두 클래스를 타입 계층
으로 묶을 수 있다고 하였다. 여기서 하나 짚고 넘어갈 것이 있다. 행동 호환성
이란 단순히 동일한 시그니처의 메서드 를 갖는다고 해서 만족되는 것이 아니라는 점이다.
행동 호환성
의 여부를 판단하는 기준은 클라이언트의 관점
이다. 단순히 동일한 메서드 시그니처를 갖는 것으로는 행동 호환성
을 만족시킬 수 없다.
- `클라이언트`가 두 타입이 동일하게 행동할 것이라고 `기대`한다면 두 타입을 `동일한 계층`으로 묶을 수 있다.
- `클라이언트`가 두 타입이 행동할 것이라 기대하지 않는다면 두 타입을 동일한 계층으로 묶어서는 안된다.
중요한 것은 클라이언트의 기대다. 타입 계층을 이해하기 위해서는 그 타입 계층이 사용될 ‘문맥’을 이해해야 한다.
클라이언트의 기대에 따른 행동 호환성과 대체 가능성
클라이언트의 기대
라는 말이 너무 추상적이라 쉽게 와닿지 않는다면 리스코프 치환 원칙
과 계약에 의한 설계
의 관점에 따라 조금 일반화 하여 정의할 수 있을 것 같다.
여기서는 예시로 직사각형(슈퍼 타입)
과 정사각형(서브 타입)
을 예로 들어보자.
클라이언트
는 직사각형.resize(width, height)
과 같은 메시지를 전송할 때 직사각형
이 width
와 height
의 값으로 재설정 하기를 기대
한다.
반면 정사각형.resize(width, height)
을 전송하게 되면 직사각형에 기대한 바와 달리 정사각형은 width, height의 값으로 각각 재설정 하지 않는다. 다시말해 정사각형은 클라이언트가 직사각형에 대해 기대한 행동과 다르게 행동한다. 따라서 정사각형
은 직사각형
의 서브타입이 될 수 없다.
위 내용은 이후에 좀 더 자세히 다루도록 하자.
클라이언트 기대에 따른 인터페이스 분리
인터페이스
는 클라이언트가 기대하는 바에 따라 분리되어야 한다. A 클래스의 클라이언트가 오직 fly
메시지만 전송한다면 이 클라이언트에게는 오직 fly
메시지만 보여야 한다. B 클래스의 클라이언트가 오직 walk
메시지만을 전송한다면 이 클라이언트에게는 오직 walk
메시지만 보여야 한다.
따라서 fly
오퍼레이션을 가진 flyer
과 walk
오퍼레이션을 가진 walker
인터페이스로 각각 분리하는 것이 적절하다.
기대하는 바가 서로 다른 클라이언트에 따라서 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있다. 대부분의 경우 인터페이스
는 클라이언트의 요구가 바뀜에 따라 변경된다. 클라이언트에 따라 인터페이스를 분리하면 각 클라이언트의 요구가 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있다.
이렇게 인터페이스를 클라이언트의 기대에 따라 분리 함으로써 변경에 의한 영향을 제어 하는 설계 원칙을 인터페이스 분리 원칙(ISP)
라고 한다.
인터페이스 분리 원칙
은 비대한 인터페이스의 단점을 해결한다. 비대한 인터페이스는 응집성이 없는 인터페이스다. 또한 비대한 인터페이스
를 갖는 클래스는 클라이언트 사이에 해로운 결합이 생기게 만든다.
클라이언트는 자신이 실제로 호출하는 메서드에만 의존해야 한다. 이것은 인터페이스를 여러 클라이언트에 특화된 인터페이스로 분리함으로써 성취할 수 있다.
서브클래싱 다시보기
다른 클래스의 코드를 재사용
하기 위한 목적으로 상속을 사용한 경우다.
자식 클래스와 부모 클래스의 행동이 호환되지 않는다. 즉, 클라이언트의 입장에서 부모 클래스에 기대한 모든 행동을 자식 클래스가 만족시키지 않는다.
클라이언트 관점에서의 행동 호환성
을 만족시키지 못하기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대신할 수 없다.
서브타이핑 다시보기
타입 계층을 구성
하기 위해 상속을 사용한 경우다.
클라이언트의 입장에서 부모 클래스에 기대하는 모든 행동을 자식 클래스가 만족시킨다.
행동 호환성
을 만족시키기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.
즉, 컴파일 시점에는 슈퍼타입으로 정의하지만 런타임 시점에 서브타입으로 대체할 수 있다.
자식 클래스가 부모 클래스를 대체할 수 있기 위해서는 자식 클래스는 부모 클래스가 사용되는 모든 문맥에서 부모 클래스와 동일하게 행동할 수 있어야 한다. 다시말해, 대체 가능해야 한다.
리스코프 치환 원칙과 서브타이핑
서브타이핑
을 만족시키기 위해서는 클라이언트의 관점
에서 행동 호환성
을 만족시켜야 한다고 하였다. 이번엔 리스코프 치환 원칙
을 통해서 의미를 좀 더 자세히 들여다보자.
- `클라이언트`가 **차이점을 인식하지 못한 채 부모 클래스의 인터페이스를 통해 자식 클래스를 사용할 수 있어야 한다.**
- `차이점을 인식하지 못한 채`라는 의미를 곱씹어보면 `클라이언트`가 `부모 클래스`에 대해 `가정`하고 `기대`하는 바를 `자식 클래스`도 충족시켜야 한다는 것을 의미한다.
- `부모 클래스에 대한 가정과 기대`를 충족시킨다는 것은 `행동 호환성`을 만족시키고 `대체 가능`하다는 것을 의미한다.
이와같이 리스코프 치환 원칙
은 자식 클래스
가 부모 클래스
를 대체하기 위해서 부모 클래스에 대한 클라이언트의 ‘가정’과 ‘기대’를 준수해야 한다는 것을 의미한다.
다시말해 대체 가능성
, 행동 호환성
을 결정하는 것은 클라이언트
이기 때문에 상속 관계
는 클라이언트
의 관점에서 자식 클래스
가 부모 클래스
를 대체할 수 있을 때만 올바르다.
구현 방법은 중요하지 않다. 핵심은 구현 방법과 무관하게 클라이언트의 관점
에서 슈퍼 타입
에 기대하는 모든것(행동이든 행동 수행 이후 객체의 상태든)
이 서브 타입
에게도 동일하게 적용되어야 한다는 것이다.
계약에 의한 설계와 서브타이핑
클라이언트의 관점
에서 부모 클래스에 가정하고 기대하는 ‘행동’을 자식 클래스가 충족시켜야 한다. 는 의미를 클라이언트와 부모 클래스간의 계약 의 관점에서 살펴보자.
계약에 의한 설계
는 클라이언트
와 부모 클래스
간에 체결한 계약
을 자식 클래스
도 준수해야 서브타이핑
관계가 만족됨을 설명한다.
결론부터 정리하자면 서브 타입
은 클라이언트
와 슈퍼 타입
간에 체결한 계약
에서
- 사전조건
을 강화시킬 수 없다.
- 사후조건
을 약화시킬 수 없다.
- 불변식
은 반드시 지켜야 한다.
- 슈퍼 타입
이 던지는 예외
와 같은 타입의 예외
만 던져야 한다.
사전 조건
사전 조건
을 지키는 것은 클라이언트
의 의무이다. 보통 사전 조건
은 파라미터의 정합성 에 대한 조건인 경우가 일반적이다.
서브타입은 사전 조건을 강화해서는 안된다. 클라이언트
와 슈퍼 타입
간에 체결한 사전 조건
을 서브 타입
이 추가하거나, 제한하는 등 더 엄격해지면 안된다.
만일 사전 조건
이 강화되면 클라이언트
와 슈퍼 타입
간에 가능했던 협력이 서브 타입
과는 불가능해지는 경우가 생기고 이는 클라이언트의 관점에서 슈퍼타입에 가정하고 기대했던 것과 다르기 때문에 서브타이핑이 될 수 없다.
사후 조건
사후 조건
을 지키는 것은 서버(슈퍼타입/서브타입)
의 의무이다. 사후 조건
은 메서드가 실행된 결과
에 대한 조건을 의미한다.
사후 조건
은 보통 메서드 실행 후 서버의 인스턴수 변수 결과
, 파라미터의 결과
, 반환 값의 결과
에 대한 조건인 경우가 일반적이다.
여기서 정사각형과 직사각형
의 예제가 바로 사후조건 위반
임을 알 수 있다. 즉, 클라이언트
가 부모 클래스(직사각형)
에 대해 resize
메시지 전송 후 인스턴스 변수인 height, width
에 대해 기대하는 결과가 자식 클래스(정사각형)
은 충족시키지 못하기 때문에 resize 문맥에서 정사각형은 직사각형의 서브타입이 아니다.
즉, 클라이언트의 관점에서 슈퍼타입에 가정하고 기대했던 것과 다르기 때문에 서브타이핑이 될 수 없다.
또한 서브타입은 사후 조건을 약화시키면 안된다. 라는 것이 중요하다. 만일 사후 조건
이 약화되어 클라이언트
가 기대하는 결과 이하를 얻게 되면, 해당 결과를 이용하는 클라이언트
가 이후의 협력에서 예상치 못한 결과로 버그가 생길 수 있다.
불변식
불변식은 메서드 실행 전
, 메서드 실행 후
서버가 반드시 지켜야 하는 내부 상태를 의미한다. 이는 자식 클래스
가 항상 부모 클래스
의 불변식을 지켜야 함을 의미한다.
사전 조건
과 사후 조건
은 실행하는 메서드에 따라서 그 조건이 다를 수 있지만 불변식
은 모든 메서드 호출에 대해 일관적으로 유지해야 하는 상태 를 의미한다.
예외 처리
자식 클래스
는 부모 클래스
가 던지는 예외
와 다른 타입의 예외를 던질 수 없다.
참고 및 출처
- 오브젝트