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

본문


들어가며

13장 서브클래싱과 서브타이핑 은 부록 계약에 의한 설계 와 함께 요약한다.

핵심 내용은 클라이언트의 관점에서 행동 호환성이 만족될 때 서브타이핑의 관계가 성립한다는 것이다.

그리고 행동 호환성을 만족시킨다는 의미가 무엇인지 리스코프 치환 원칙계약에 의한 설계를 통해 설명한다.


상속은 코드 재사용을 목적으로 사용하면 안된다.

코드 재사용을 위해 사용된 상속서브 클래싱이라고 한다.

서브클래싱은 컴파일 타임에 부모 클래스와 자식 클래스가 정적으로 결합 하고, 이런 특성 때문에 런타임에 동적으로 관계 변경이 불가능하다.

만일 코드 재사용을 목적으로 상속을 사용하게 되면, 취약한 기반 클래스 문제가 발생할 가능성이 높아지는데, 이는 설계의 진화와 변경을 방해한다.

  • 취약한 기반 클래스 문제
    • 부모 클래스의 내부 구현 변경에 자식 클래스까지 영향을 받는다.
      • 부모 클래스 내부 메서드 호출 순서와 같은 구체적인 내부 구현 사항 변경에 부모 클래스의 코드를 재사용 하는 자식 클래스 까지 파급 효과가 미친다.
      • 자식 클래스가 부모 클래스의 변경에 발맞춰 함께 변경되어야 하는 상황이 생긴다.
    • 부모 클래스의 개발자가 세운 가정을 정확히 파악하고 부모 클래스의 내부 구현에 대해 알아야 한다.
      • 부모 클래스의 가정을 정확히 파악하고 자식 클래스를 개발해야 하기 때문에 캡슐화가 저해된다.
      • 부모 클래스 내부 구현 상 메서드 호출 순서가 존재하거나, 메서드 호출에 따른 상태 변경 등의 상세 구현 내용을 정확히 파악해야 한다.


상속을 사용하는 목적은 타입 계층의 구현이어야 한다.

타입 계층의 구현을 위한 상속을 서브 타이핑이라고 한다. 타입 계층을 목표로 상속을 사용하면 다형적인 협력이 가능한 객체들의 관계 를 얻음으로써 유연한 설계를 구현할 수 있다.

타입이란

- `타입`이란 **공통의 특징을 공유하는 대상들의 분류**
- `타입`이란 **동일한 오퍼레이션을 적용할 수 있는 객체들의 집합**

위 정의에서 알 수 있듯이 타입을 정의하는 가장 중요한 키워드는 바로 오퍼레이션이다.

타입호출 가능한 오퍼레이션의 집합 에 의해 정의된다. 쉽게 말하자면, 어떤 객체가 특정 타입이기 위해서는 해당 타입의 오퍼레이션을 수행할 수 있어야 한다.

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

- **객체의 퍼블릭 인터페이스가 객체의 타입을 결정한다.**
- **동일한 퍼블릭 인터페이스를 갖는 객체는 동일한 타입으로 간주할 수 있다.**


올바른 상속, 올바른 타입 계층

올바른 상속 이란 타입 계층의 구현을 위한 상속을 의미한다. 그렇다면 타입 계층을 구현한다는 의미는 무엇일까?

  • 클라이언트 입장에서 슈퍼 타입서브 타입의 차이점을 모르고 사용할 수 있어야 한다.
    • 클라이언트의 관점에서 두 클래스에 기대하는 행동이 같아야 한다.
    • 슈퍼 타입서브 타입간에 행동 호환성이 만족되어야 타입 계층을 위한 상속이라고 할 수 있다.


행동 호환성의 의미

행동 호환성이 만족되어야만 두 클래스를 타입 계층으로 묶을 수 있다고 하였다. 여기서 하나 짚고 넘어갈 것이 있다. 행동 호환성이란 단순히 동일한 시그니처의 메서드 를 갖는다고 해서 만족되는 것이 아니라는 점이다.

행동 호환성의 여부를 판단하는 기준은 클라이언트의 관점이다. 단순히 동일한 메서드 시그니처를 갖는 것으로는 행동 호환성을 만족시킬 수 없다.

- `클라이언트`가 두 타입이 동일하게 행동할 것이라고 `기대`한다면 두 타입을 `동일한 계층`으로 묶을 수 있다.
- `클라이언트`가 두 타입이 행동할 것이라 기대하지 않는다면 두 타입을 동일한 계층으로 묶어서는 안된다.

중요한 것은 클라이언트의 기대다. 타입 계층을 이해하기 위해서는 그 타입 계층이 사용될 ‘문맥’을 이해해야 한다.


클라이언트의 기대에 따른 행동 호환성과 대체 가능성

클라이언트의 기대라는 말이 너무 추상적이라 쉽게 와닿지 않는다면 리스코프 치환 원칙계약에 의한 설계의 관점에 따라 조금 일반화 하여 정의할 수 있을 것 같다.

여기서는 예시로 직사각형(슈퍼 타입)정사각형(서브 타입)을 예로 들어보자.

클라이언트직사각형.resize(width, height)과 같은 메시지를 전송할 때 직사각형widthheight의 값으로 재설정 하기를 기대한다.

반면 정사각형.resize(width, height)을 전송하게 되면 직사각형에 기대한 바와 달리 정사각형은 width, height의 값으로 각각 재설정 하지 않는다. 다시말해 정사각형은 클라이언트가 직사각형에 대해 기대한 행동과 다르게 행동한다. 따라서 정사각형직사각형의 서브타입이 될 수 없다.

위 내용은 이후에 좀 더 자세히 다루도록 하자.


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

인터페이스클라이언트가 기대하는 바에 따라 분리되어야 한다. A 클래스의 클라이언트가 오직 fly 메시지만 전송한다면 이 클라이언트에게는 오직 fly 메시지만 보여야 한다. B 클래스의 클라이언트가 오직 walk 메시지만을 전송한다면 이 클라이언트에게는 오직 walk 메시지만 보여야 한다.

따라서 fly 오퍼레이션을 가진 flyerwalk 오퍼레이션을 가진 walker 인터페이스로 각각 분리하는 것이 적절하다.

기대하는 바가 서로 다른 클라이언트에 따라서 인터페이스를 분리하면 변경에 대한 영향을 더 세밀하게 제어할 수 있다. 대부분의 경우 인터페이스는 클라이언트의 요구가 바뀜에 따라 변경된다. 클라이언트에 따라 인터페이스를 분리하면 각 클라이언트의 요구가 바뀌더라도 영향의 파급 효과를 효과적으로 제어할 수 있다.

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

인터페이스 분리 원칙은 비대한 인터페이스의 단점을 해결한다. 비대한 인터페이스는 응집성이 없는 인터페이스다. 또한 비대한 인터페이스를 갖는 클래스는 클라이언트 사이에 해로운 결합이 생기게 만든다.

클라이언트는 자신이 실제로 호출하는 메서드에만 의존해야 한다. 이것은 인터페이스를 여러 클라이언트에 특화된 인터페이스로 분리함으로써 성취할 수 있다.


서브클래싱 다시보기

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

자식 클래스와 부모 클래스의 행동이 호환되지 않는다. 즉, 클라이언트의 입장에서 부모 클래스에 기대한 모든 행동을 자식 클래스가 만족시키지 않는다.

클라이언트 관점에서의 행동 호환성을 만족시키지 못하기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대신할 수 없다.


서브타이핑 다시보기

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

클라이언트의 입장에서 부모 클래스에 기대하는 모든 행동을 자식 클래스가 만족시킨다.

행동 호환성을 만족시키기 때문에 자식 클래스의 인스턴스가 부모 클래스의 인스턴스를 대체할 수 있다.

즉, 컴파일 시점에는 슈퍼타입으로 정의하지만 런타임 시점에 서브타입으로 대체할 수 있다.

자식 클래스가 부모 클래스를 대체할 수 있기 위해서는 자식 클래스는 부모 클래스가 사용되는 모든 문맥에서 부모 클래스와 동일하게 행동할 수 있어야 한다. 다시말해, 대체 가능해야 한다.


리스코프 치환 원칙과 서브타이핑

서브타이핑을 만족시키기 위해서는 클라이언트의 관점에서 행동 호환성을 만족시켜야 한다고 하였다. 이번엔 리스코프 치환 원칙을 통해서 의미를 좀 더 자세히 들여다보자.

- `클라이언트`가 **차이점을 인식하지 못한 채 부모 클래스의 인터페이스를 통해 자식 클래스를 사용할 수 있어야 한다.**
    - `차이점을 인식하지 못한 채`라는 의미를 곱씹어보면 `클라이언트`가 `부모 클래스`에 대해 `가정`하고 `기대`하는 바를 `자식 클래스`도 충족시켜야 한다는 것을 의미한다.
    - `부모 클래스에 대한 가정과 기대`를 충족시킨다는 것은 `행동 호환성`을 만족시키고 `대체 가능`하다는 것을 의미한다.

이와같이 리스코프 치환 원칙자식 클래스부모 클래스를 대체하기 위해서 부모 클래스에 대한 클라이언트의 ‘가정’과 ‘기대’를 준수해야 한다는 것을 의미한다.

다시말해 대체 가능성, 행동 호환성을 결정하는 것은 클라이언트이기 때문에 상속 관계클라이언트의 관점에서 자식 클래스부모 클래스를 대체할 수 있을 때만 올바르다.

구현 방법은 중요하지 않다. 핵심은 구현 방법과 무관하게 클라이언트의 관점에서 슈퍼 타입에 기대하는 모든것(행동이든 행동 수행 이후 객체의 상태든)서브 타입에게도 동일하게 적용되어야 한다는 것이다.


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

클라이언트의 관점에서 부모 클래스에 가정하고 기대하는 ‘행동’을 자식 클래스가 충족시켜야 한다. 는 의미를 클라이언트와 부모 클래스간의 계약 의 관점에서 살펴보자.

계약에 의한 설계클라이언트부모 클래스간에 체결한 계약자식 클래스도 준수해야 서브타이핑 관계가 만족됨을 설명한다.

결론부터 정리하자면 서브 타입클라이언트슈퍼 타입간에 체결한 계약에서 - 사전조건을 강화시킬 수 없다. - 사후조건을 약화시킬 수 없다. - 불변식은 반드시 지켜야 한다. - 슈퍼 타입이 던지는 예외같은 타입의 예외만 던져야 한다.


사전 조건

사전 조건을 지키는 것은 클라이언트의 의무이다. 보통 사전 조건파라미터의 정합성 에 대한 조건인 경우가 일반적이다.

서브타입은 사전 조건을 강화해서는 안된다. 클라이언트슈퍼 타입간에 체결한 사전 조건서브 타입이 추가하거나, 제한하는 등 더 엄격해지면 안된다.

만일 사전 조건이 강화되면 클라이언트슈퍼 타입간에 가능했던 협력이 서브 타입과는 불가능해지는 경우가 생기고 이는 클라이언트의 관점에서 슈퍼타입에 가정하고 기대했던 것과 다르기 때문에 서브타이핑이 될 수 없다.


사후 조건

사후 조건을 지키는 것은 서버(슈퍼타입/서브타입)의 의무이다. 사후 조건은 메서드가 실행된 결과에 대한 조건을 의미한다.

사후 조건은 보통 메서드 실행 후 서버의 인스턴수 변수 결과, 파라미터의 결과, 반환 값의 결과에 대한 조건인 경우가 일반적이다.

여기서 정사각형과 직사각형의 예제가 바로 사후조건 위반임을 알 수 있다. 즉, 클라이언트부모 클래스(직사각형)에 대해 resize 메시지 전송 후 인스턴스 변수인 height, width에 대해 기대하는 결과가 자식 클래스(정사각형)은 충족시키지 못하기 때문에 resize 문맥에서 정사각형은 직사각형의 서브타입이 아니다.

즉, 클라이언트의 관점에서 슈퍼타입에 가정하고 기대했던 것과 다르기 때문에 서브타이핑이 될 수 없다.

또한 서브타입은 사후 조건을 약화시키면 안된다. 라는 것이 중요하다. 만일 사후 조건이 약화되어 클라이언트가 기대하는 결과 이하를 얻게 되면, 해당 결과를 이용하는 클라이언트가 이후의 협력에서 예상치 못한 결과로 버그가 생길 수 있다.


불변식

불변식은 메서드 실행 전, 메서드 실행 후 서버가 반드시 지켜야 하는 내부 상태를 의미한다. 이는 자식 클래스가 항상 부모 클래스의 불변식을 지켜야 함을 의미한다.

사전 조건사후 조건은 실행하는 메서드에 따라서 그 조건이 다를 수 있지만 불변식모든 메서드 호출에 대해 일관적으로 유지해야 하는 상태 를 의미한다.


예외 처리

자식 클래스부모 클래스가 던지는 예외와 다른 타입의 예외를 던질 수 없다.

참고 및 출처

  • 오브젝트