(OOP) 서브클래싱과 서브타이핑 고찰


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

상속은 컴파일 타임에 부모클래스와 자식클래스를 정적으로 결합한다. 이런 특성 때문에 런타임에 동적으로 변경이 불가능하다.

만일 코드 재사용을 목적으로 상속을 사용하게 된다면 취약한 기반 클래스 문제가 발생할 확률이 높아진다. 이에 따라 설계의 변경과 진화를 방해한다.

  • 부모클래스의 내부 구현 변경에 자식클래스까지 영향을 미친다.
    • 부모클래스 내부 메서드 호출 순서가 변경되거나 내부 구현상 개발자의 가정이 변경되는 경우, 부모클래스의 코드를 재사용하는 자식클래스의 내부 구현도 변경될 수 있다.
    • 자식클래스가 부모클래스의 변경에 발맞춰 함께 변경되어야 하는 상황이 생긴다.
  • 부모클래스의 개발자가 세운 가정을 정확히 파악하고, 부모클래스 내부 구현을 알아야 한다.
    • 부모클래스의 가정에 발맞춰 자식클래스를 개발해야 하기 때문에 캡슐화가 저해된다.
    • 부모클래스 내부 구현상 메서드 호출의 순서가 존재하거나, 메서드 호출에 따른 상태 변경등의 상세 구현 내용을 정확히 파악해야 한다.
  • 일반적으로 코드 재사용내부 구현에 해당한다. 그런데 코드 재사용을 위해 상속을 사용하게 되면 클라이언트에게 부모클래스의 인터페이스까지 노출되게 된다.
  • 상속을 사용하게 되면 런타임에 동적으로 변경이 불가능하다.


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

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


타입이란

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

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

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

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

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


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

올바른 상속을 위해서는 타입 계층을 구현하기 위해서 사용해야 한다. 그렇다면 타입 계층을 구현한다는 의미는 무엇일까?

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


행동 호환성의 의미

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

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

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


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


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

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

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

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

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

위 문제는 이후에 좀 더 자세히 다루도록 하자.


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

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

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

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

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

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


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


서브클래싱

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

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

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


서브타이핑

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

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

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

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

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


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

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

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

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

리스코프 치환 원칙상속 관계에 있는 두 클래스 사이의 관계를 클라이언트와 떨어뜨려 놓지 말라고 강조한다.

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

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


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

지난 시간에 서브타이핑을 만족시키기 위해서는 클라이언트의 관점에서 부모 클래스에 대해 가정하고 부모 클래스에 대해 기대하는 ‘행동’을 자식 클래스가 충족시켜야 한다. 고 하였다.

그리고 이를 행동 호환성, 대체 가능성이라는 키워드로 정리했다.

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

먼저 결론부터 정리하자면 서브타입클라이언트슈퍼 타입간에 체결한 계약에서

  • 사전조건을 강화시킬 수 없다.
  • 사후조건을 약화시킬 수 없다.
  • 불변식은 반드시 지켜야 한다.
  • 슈퍼 타입이 던지는 예외같은 타입의 예외만 던져야 한다.

로 요약할 수 있다.


사전 조건

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

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

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


사후조건

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

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

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

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

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


불변식

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

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


예외 처리

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