들어가며

내 마음속의 멘토이자 오브젝트 책의 저자인 조영호 님과의 메일 문답 중 포워딩과 위임의 차이에 대한 내용에서 아래와 같은 답변을 받았다.

단순히 합성 관계로 연결되었다고 해서 ‘위임’이라고 부르지는 않고 ‘올바른 상속 관계’를 합성으로 변경했을 경우에만 ‘위임’이라고 부를 수 있습니다. 여기서 올바른 상속관계란 ‘올바른 IS-A 관계’, 즉 개념적으로 리스코프 치환 원칙을 만족하면서 대체 가능한 객쳐여야 합니다. (후략)

정말 감사하고 친절한 답변을 주셨다. 문제는 정말 쉽게 설명해주신 답변임에도 불구하고 내가 한참 부족하여 리스코프 치환 원칙에 대해 잘 모르고 있었다는 사실이다.

답변해주신 내용을 조금이나마 더 내것으로 만들기 위해서는 먼저 리스코프 치환 원칙에 대해 알아볼 필요가 있다.


리스코프 치환 원칙

_ - 개방-폐쇄 원칙을 받쳐주는 다형성에 관한 원칙을 제공한다._

_ - 상속과 관련된 원칙이다._

_ - 서브 타입(자식)은 언제나 자신의 기반 타입(부모)로 교체할 수 있어야 한다._

_ 기반 클래스(List)에서 유도된 클래스(ArrayList, LinkedList)에 대해 아무것도 알 필요 없어야 한다. (행위가 변하면 안된다.)_

_ 참조되는 기반 클래스의 함수는 파생 클래스 객체의 상세를 알지 않고서라도 사용될 수 있어야 한다._

_ 상위 타입에서 명세한 값, 기능, 예외 이외에는 다른 행동을 하지 못하게 해야한다._

_ 자식 클래스는 최소한 부모 클래스에서 가능한 행동은 수행 가능해야 한다._

_ 자식 클래스는 부모 클래스의 ‘역할’을 충실히 하면서 확장해야 한다. _

객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.

S타입의 객체 o1T타입의 객체 o2가 있고 T 타입을 이용하는 프로그램(어플리케이션, 모듈 등?) P에서 o2의 빈 자리에 o1로 치환하더라도 프로그램의 행위(정확성, 수행하는 업무 등)가 변하지 않으면 ST의 하위 타입이 될 수 있다.

리스코프 치환 원칙의 위배는 주로 상위 클래스의 구현된 메서드를 오버라이딩 시 발생한다. 다시말해 상위 타입의 명세를 잃어버린다면 상속 관계가 아니다.

이를 기반으로 요약해 보았을 때 자식 클래스는 부모 클래스의 역할을 충실히 수행 해야 한다고 할 수 있겠다.

명세의 측면에서 나름대로 살펴본 결과 아래와 같은 상황에 해당할 때(어플리케이션의 문맥에 따라 다양하지만) 리스코프 치환 원칙에 위배된다고 볼 수 있겠다.

명시된 명세에서 벗어난 값을 리턴하는 경우.

명시된 명세에서 벗어난 예외를 발생시키는 경우.

명시된 명세에서 벗어난 기능을 수행하는 경우


코드 예시

위의 코드에서 class PdoSomthing(T p)를 주시하자.

doSomthing(T p)p로 들어오는 애가 T타입의 인스턴스인 p가 들어오던 아니면 서브타입 S의 인스턴스인 c가 들어오던 실행에는 전혀 영향을 끼치면 안된다. 즉, 어떤 타입의 인스턴스가 들어오는지 전혀 알 필요 없이! 수행 가능해야 한다.

객체지향에서 주로 재사용을 할 때 슈퍼클래스에 많은 기능을 넣어놓고 서브 클래스에서 상속하여 이를 사용하면서 재사용이라는 표현을 많이 한다. 하지만 이런 코드는 결국 다운 캐스트를 많이 하게된다.

다운 캐스트를 사용하면 ‘타입’에 대한 의존성이 생긴다. 즉, 다운캐스트는 사용되어서는 안된다.


OCP vs LSP

  • OCP(개방-폐쇄 원칙)
    • abstraction(추상화), polymorphism(inheritance)를 이용해서 구현
    • 추상화에 의존
  • LSP
    • OCP를 받쳐주는 polymorphism에 관한 원칙을 제공
    • LSP가 위반되면 OCP도 위반된다.
    • LSP를 위반하면 서브타입이 추가될 때 마다 클라이언트들이 수정되어야 한다.
    • instanceof / downcasting을 사용하는 것은 전형적인 LSP 위반의 징조이다.

OCP는 호출될 때 polymorphism이 일어난다. 즉, 실제 구현체가 뭔지 모른다. 이들을 추상화하여 이쁜 인터페이스를 표현.

LSP는 polymorphism에 대한 원칙을 제공한다. 즉 슈퍼타입이나 서브타입에서 다운캐스트 등을 통해 정확한 타입이 무엇인지 알아야 하는 상황이 와서는 안된다.

또한 서브 타입에서 슈퍼 타입을 사용하는것 (super 참조) 역시 아주 좋지 못한 상황이다. 만일 슈퍼타입을 사용해야 하는 경우가 나타나면 어떻게 해서든 우회하는 것이 좋다.


Rectangle 예제

리스코프 치환 원칙에서 가장 많이 사용되는 Rectangle 예제를 살펴보자.

  • Rectangle(직사각형)은 시스템의 여러곳에 퍼져있다.
  • Square(정사각형)을 서브 타입으로 추가하려고 한다.
  • Square IS-A Rectangle로 상속 관계로 추가해보자.

정사각형은 사각형이다. 이건 IS-A 관계가 논리적으로 맞는 것 같다. 따라서 상속 관계가 맞는 것 같다.

하지만 실제 행위를 구현해보면 area를 구할 때 서로 다른 결과를 출력한다.

따라서 클라이언트 입장에서 협력하는 객체의 타입이 Square인지 Rectangle인지를 알아야만 하는 상황이 생긴다. 즉, inastanceof 또는 다운 캐스팅을 사용해야 하는 상황이 생겨버리는 것이다.


예시 코드

위의 Rectangle과 유사하지만 좀 더 와닿는 예시가 있어서 추가해보았다.

앞서 계속 언급했듯 리스코프 치환 원칙을 위배하지 않는 상속이란 다음과 같다.

_ 서브타입은 언제나 자신의 상위 타입으로서의 기능을 해야한다._

만일 상위 클래스가 Rectangle(직사각형)이고 하위 클래스가 Square(정사각형)이라고 해보자.

직사각형(Rectangle)은 마음대로 크기 조절이 가능하지만 정사각형(Square)은 크기 조절에 제약이 있다.

일견 문제가 없어보이지만 문제는 정사각형이 직사각형의 문맥에서 사용될 때다.

클라이언트는 직사각형이라고 생각하고 인자를 처리하고 있다.

하지만 인자로 직사각형이 아니라 정사각형이 들어온다면 기대와 다른 기능을 수행하거나 보시다시피 예외를 뱉어낼것이다.