S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위타입이다
잘 와닿지 않는다. 우리가 많이 들어본 LSP를 다시 생각해보자
일반적으로는 자바에서 말하는 상속을 잘 구현한 예들 (is a 관계라고 흔히 말하는) 이 리스코프 치환 원칙을 잘 지킨 가장 간단한 예 일 것이다.
그런데, 우리는 객체간 어떤 요소들이 is-a 관계일 때 적절한 상속이라고 말할 수 있을까?
penguin ( is a ) bird
라는 상속 관계를 만들었다고 생각 해보자. 실생활에서 펭귄이 새라는 것은 모두 인정하는 바일 테지만, 우리가 프로그램에서 펭귄이 새를 상속하도록 (혹은 새의 인터페이스를 구현하도록) 만드는 데는 타당한 이유가 반드시 필요하다.
예를 들어, 우리가 실생활에서 펭귄이 새라는것을 인지하는 이유는
위와 같은 인터페이스(동작) 을 펭귄이 가지고 있기 때문도 있을 것이다.
그런데, 우리 프로그램에서 정의한 새 인터페이스(동작)가 다음과 같다고 생각해보자
이 경우 우리 프로그램에서 펭귄은 새를 상속 (혹은 구현) 할 수 있을까? 불가능 할 것이다.
위에서 볼 수 있듯, 리스코프 치환 원칙은 좁게 보면 상속 이지만, 더 적절하게 이해하기 위해서는 인터페이스, 즉 동작(행위) 를 얼마나 기대에 맞게 구현했느냐에 관한 문제이다.
만약 우리가 어떻게든 펭귄을 새로 만들기 위해 (실제 펭귄은 새이므로) 하늘을 난다 인터페이스를 헤엄을 친다 라는 동작으로 구현한다고 해보자.
"새" 라는 인터페이스를 선언하고, "하늘을 난다" 라는 기능을 기대하는 사용자가 (구현체가 몇미터를 날 수 있는지, 얼마나 높이 나는지는 상관 없이 "하늘을 난다" 라는 동작을 기대한다) 이 인터페이스를 믿고 사용 할 수 있을까? 불가능하다. 이 경우 인터페이스를 둠으로써 기대하는 다형성 도 지켜지지 않을 것이고, 인터페이스 자체의 동작을 신뢰하는 것이 불가능 할 것이다. 그에 따라 다시 이 인터페이스를 신뢰 가능한 인터페이스로 만들기 위해 과도하게 복잡한 공정들이 구현체 스스로나 인터페이스에 추가 될 가능성이 있다.
즉, 리스코프 치환 원칙은 인터페이스(동작) 을 신뢰할 수 있는 상태로 남기라는 것, 그리고 그를 위해서 이 인터페이스(동작) 을 수행하는 모든 모듈, 컴포넌트, 객체들이 이 인터페이스의 동작을 정확히 수행하도록 만들라는 것
"LSP는 아키텍쳐 수준까지 확장할 수 있고, 확장해야만 한다..."
"치환 가능성을 조금이라도 위배하면 시스템 아키텍쳐가 오염되어 상당량의 별도 매커니즘을 추가해야 할 수 있기 때문이다."