본문 바로가기

기술서적 정리/오브젝트 - 코드로 이해하는 객체지향 설계

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

https://wikibook.co.kr/object/

 

오브젝트: 코드로 이해하는 객체지향 설계

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

wikibook.co.kr

 

 

동일한 메시지에 대해 서로 다르게 행동할 수 있는 다형적인 객체를 구현하기 위해서는 객체의 행동을 기반으로 타입 계층을 구성해야 한다.

상속의 가치는 이러한 타입 계층을 구현할 수 있는 쉽고 편안한 방법을 제공하는 데 있다.

타입 사이의 관계를 고려하지 않은 채 단순히 코드를 재사용하기 위해 상속을 사용해서는 안된다.

 

이번 장에서는 올바른 타입 계층을 구성하는 원칙을 살펴보기로 한다.

이번 장을 읽고 나면 상속이 서브타입 다형성과 동적 메서드 탐색에 밀접하게 연관돼 있다는 사실을 알게 될 것이다.

 

 

◈ 타입

개념 관점의 타입

어떤 대상이 타입으로 분류될 때 그 대상을 타입의 인스턴스라고 부른다.

일반적으로 타입의 인스턴스를 객체라고 부른다.

 

 

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

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

타입은 적용 가능한 오퍼레이션의 종류와 의미를 정의함으로써 코드의 의미를 명확하게 전달하고 개발자의 실수를 방지하기 위해 사용된다.

 

 

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

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

1. 개념 관점에서 타입이란 공통의 특징을 공유하는 대상들의 분류다.

2. 프로그래밍 언어 관점에서 타입이란 동일한 오퍼레이션을 적용할 수 있는 인스턴스들의 집합이다.

 

객체지향 프로그래밍에서 타입을 정의하는 것은 객체의 퍼블릭 인터페이스를 정의하는 것과 동일하다.

 

객체지향에서는 객체가 수신할 수 있는 메시지를 기준으로 타입을 분류하기 때문에 동일한 퍼블릭 인터페이스를 가지는 객체들은 동일한 타입으로 분류할 수 있다.

 

객체에게 중요한 것은 속성이 아니라 행동이다.

어떤 객체들이 동일한 상태를 가지고 있더라도 퍼블릭 인터페이스가 다르다면 이들은 서로 다른 타입으로 분류된다.

반대로 어떤 객체들이 내부 상태는 다르지만 동일한 퍼블릭 인터페이스를 공유한다면 이들은 동일한 타입으로 분류된다.

객체의 타입을 결정하는 것은 내부의 속성이 아니라 객체가 외부에 제공하는 행동이라는 사실을 기억하라.




 

◈ 타입 계층

타입 사이의 포함관계

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

 

 

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

일반적인 타입과 구체적인 타입 간의 관계를 형성하는 기준은 ‘퍼블릭 인터페이스’이다.

 

서브타입의 인스턴스는 슈퍼타입의 인스턴스로 간주될 수 있다.

이 사실이 이번 장의 핵심이다.

그리고 상속과 다형성의 관계를 이해하기 위한 출발점이다.

 

 

 

 

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

언제 상속을 사용해야 하는가?

상속의 올바른 용도는 타입 계층을 구현하는 것이다.

 

마틴 오더스키는 다음과 같은 질문을 해보고 두 질문에 모두 ‘예’라고 답할 수 있는 경우에만 상속을 사용하라고 조언한다.

 

1. 상속 관계가 IS-A 관계를 모델링하는가?

일반적으로 “자식 클래스는 부모 클래스다” 라고 말해도 이상하지 않다면 상속을 사용할 후보로 간주할 수 있다.

 

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

클라이언트 입장에서 부모 클래스와 자식 클래스의 차이점을 몰라야 한다.

이를 자식 클래스와 부모 클래스 사이의 행동 호환성이라고 부른다.

 

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

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

 

 

IS-A 관계

“펭귄은 새고, 따라서 날 수 있다.”

 

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

 

타입 계층의 의미는 행동이라는 문맥에 따라 달라질 수 있다.

슈퍼타입과 서브타입 관계에서는 IS-A 보다 행동 호환성이 더 중요하다.

 

어떤 두 대상을 언어적으로 IS-A라고 표현할 수 있더라도 일단은 상속을 사용할 예비 후보 정도로만 생각하라.

애플리케이션 안에서 두 가지 후보 개념이 어떤 방식으로 사용되고 협력하는지 살펴본 후에 상속의 적용여부를 결정해도 늦지 않다.

 


행동 호환성

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

클라이언트가 두 타입이 동일하게 행동할 것이라고 기대한다면 두 타입을 타입 계층으로 묶을 수 있다.

 

펭귄이 새의 서브타입이 아닌 이유는 클라이언트 입장에서 모든 새가 날 수 있다고 가정하기 때문이다.



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

행동 호환성을 만족시키지 않는 상속 계층을 그대로 유지한 채 클라이언트의 기대를 충족시킬 수 있는 방법을 찾기란 쉽지 않다.

문제를 해결할 수 있는 방법은 클라이언트의 기대에 맞게 상속 계층을 분리하는 것뿐이다.

 

클라이언트에 따라 인터페이스를 분리하면 각 클라이언트의 요구가 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있게 된다.

 

인터페이스를 클라이언트의 기대에 따라 분리함으로써 변경에 의해 영향을 제어하는 설계 원칙을 인터페이스 분리 원칙이라고 부른다.

 

현재의 요구사항이 날 수 있는 행동에 관심이 없다면 상속 계층에 플라잉버드를 추가하는 것은 설계를 불필요하게 복잡하게 만든다.

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

 

요점은 자연어에 현혹되지 말고 요구사항 속에서 클라인트가 기대하는 행동에 집중하는 것이다.

 

 

서브클래싱과 서브타이핑

서브클래싱

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

자식 클래스와 부모 클래스의 행동이 호환되지 않기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 없다.

구현상속 또는 클래스 상속이라고 부르기도 한다.



서브타이핑

타입 계층을 구성하기 위해 상속을 사용하는 경우.

서브타이핑에서는 자식 클래스와 부모 클래스의 행동이 호환되기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.

이때 부모 클래스는 자식 클래스의 슈퍼타입이 되고 자식 클래스는 부모 클래스의 서브타입이 된다.

인터페이스 상속이라고 부르기도 한다.



서브타이핑 관계가 유지되기 위해서는 서브타입이 슈퍼타입이 하는 모든 행동을 동일하게 할 수 있어야 한다.

즉, 어떤 타입의 서브타입이 되기 위해서는 행동 호환성을 만족시켜야 한다.



자식 클래스와 부모 클래스 사이의 행동 호환성은 부모 클래스에 대한 자식 클래스의 대체 가능성을 포함한다.

 

 

 

 

◈ 리스코프 치환 원칙

“서브타입은 그것의 기반 타입에 대해 대체 가능해야한다.”

클라이언트가 차이점을 인식하지 못한 채 기반 클래스의 인터페이스를 통해 서브클래스를 사용할 수 있어야 한다.”

 

리스코프 치환 원칙은 앞에서 논의한 행동 호환성을 설계 원칙으로 정리한 것이다.

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

 

정사각형과 직사각형의 상속 관계는 리스코프 치환 원칙을 위반하는 고전적인 사례 중 하나다.

 

Rectangle과 협력하는 클라이언트는 직사각형의 너비와 높이가 다르다고 가정한다.

정사각형의 너비와 높이는 항상 더 나중에 설정된 높이의 값으로 설정되는 것으로 코드를 작성하였다면 너비와 높이값을 다르게 설정할 경우 너비 높이 모두 높이값이 되어 길이를 다르게 입력하려고 실행한 메서드 의도가 실패하고 만다.

리사이즈 메서드의 관점에서 Rectangle 대신 Square를 사용할 수 없기 때문에 Square는 Rectangle이 아니다.

Square는 Rectangle의 구현을 재사용하고 있을 뿐이다.

두 클래스는 리스코프 치환 원칙을 위반하기 때문에 서브타이핑 관계가 아니라 서브 클래싱 관계다.

 

Rectangle은 ‘IS-A’라는 말이 얼마나 우리의 직관에서 벗어날 수 있는지를 잘 보여준다.

중요한 것은 클라이언트 관점에서 행동이 호환되는지 여부다.

그리고 행동이 호환될 경우에만 자식 클래스가 부모 클래스를 대신 사용할 수 있다. 

 

 

클라이언트와 대체 가능성

리스코프 치환 원칙은 자식 클래스가 부모 클래스를 대체하기 위해서는 부모 클래스에 대한 클라이언트 가정을 준수해야 한다는 것을 강조한다.

Square를 Rectangle의 자식 클래스로 만드는 것은 Rectangle에 대해 클라이언트가 세운 가정을 송두리째 뒤흔드는 것이다.

 

스택과 벡터가 서브타이핑 관계가 아니라 서브클래싱 관계인 이유도 마찬가지다.

스택과 벡터가 리스코프 치환 원칙을 위반하는 가장 큰 이유는 상속으로 인해 스택에 포함돼서는 안되는 벡터의 퍼블릭 인터페이스가 스택의 퍼블릭 인터페이스에 포함됐기 때문이다.

 

리스코프 치환 원칙클라이언트와 격리한 채로 본 모델은 의미 있게 검증하는 것이 불가능하다는 아주 중요한 결론을 이끈다.

어떤 모델의 유효성은 클라이언트의 관점에서만 검증 가능하다.

 

리스코프 치환 원칙은 상속 관계에 있는 두 클래스 사이의 관계를 클라이언트와 떨어트려 놓고 판단하지 말라고 속삭인다.

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

 

대체 가능성을 결정하는 것은 클라이언트다.

 

 

리스코프 치환 원칙은 유연한 설계의 기반이다.

리스코프 치환 원칙은 클라이언트가 어떤 자식 클래스와도 안정적으로 협력할 수 있는 상속 구조를 구현할 수 있는 가이드라인을 제공한다.

클라이언트 입장에서 퍼블릭 인터페이스의 행동 방식이 변경되지 않는다면 클라이언트의 코드를 변경하지 않고도 새로운 자식 클래스와 협력할 수 있게 된다는 것이다.

 

자식 클래스가 클라이언트의 관점에서 부모 클래스를 대체할 수 있다면 기능 확장을 위해 자식 클래스를 추가하더라도 코드를 수정할 필요가 없어진다.

따라서 리스코프 치환 원칙은 개방-폐쇄 원칙을 만족하는 설계를 위한 전제 조건이다.

일반적으로 리스코프 치환 원칙 위반은 잠재적인 개방-폐쇄 원칙 위반이다.

 


타입 계층과 리스코프 치환 원칙

핵심은 구현 방법과 무관하게 클라이언트의 관점에서 슈퍼타입에 대해 기대하는 모든 것이 서브타입에게도 적용돼야 한다는 것이다.

 

리스코프 치환 원칙을 위반하는 예를 설명하는 데 클래스 상속을 자주 사용하는 이유는 대부분의 객체지향 언어가 구현 단위로서 클래스를 사용하고 코드 재사용의 목적으로 상속을 지나치게 남용하는 경우가 많기 때문이다.