9. LSP - 리스코프 치환 원칙
이번엔 SOLID의 L에 해당하는 리스코프 치환 원칙(Liskov substitution principle) 이다.
바바라 리스코프는 하위 타입을 아래와 같이 정의했다.
S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지않는다면, S는 T의 하위 타입이다.
이 개념을 이해하기 위해 몇 가지 예제를 살펴보자.
9.1. 상속을 사용하도록 가이드하기
아래 그림와 같은 License
라는 클래스가 있다고 해보자.
License
클래스는 calcFee()
라는 메서드를 가지며, Billing
애플리케이션에서 이 메서드를 호출한다.
License
클래스에는 PersonalLicense
와 BusinessLicense
라는 두 가지 하위 타입이 존재한다.
두 하위 타입은 서로 다른 알고리즘을 이용해서 라이선스 비용을 계산한다.
이 설계는 리스코프 치환 원칙을 준수하는 데, Billing
애플리케이션의 행위가 License
클래스 타입 중 무엇을 사용하는 지에 전혀 의존하지않기 때문이다.
따라서 이들 하위 타입은 모두 License
타입을 치환할 수 있다.
9.2. 정사각형/직사각형 문제
이번엔 리스코프 치환 원칙을 위배하는 케이스를 살펴보자.
전형적인 예시로 정사각형/직사각형 문제가 있다.
이 예제에서 Square
는 Rectangle
의 하위 타입으로는 적합하지 않은데, Rectangle
의 높이와 너비는 서로 독립적으로 변경될 수 있는 반면,
Square
의 높이와 너비는 반드시 함께 변경되기 때문이다.
User
입장에서는 참조중인 타입이 Rectangle
이므로 아래와 같은 혼란이 생성된다.
1 | val rectangle: Rectangle = Square() |
실제 정사각형의 높이는 4이므로 위 테스트 코드는 실패하게 될 것이다.
이런 형태의 리스코프 치환 원칙 위반을 막기위한 유일한 방법은 타입 체크 로직을 User
에 추가하는 것이다.
이 방식은 User
의 행위가 사용하는 타입에 의존하게 되므로, 결국 타입을 서로 치환할 수 없게 된다.
9.3. 리스코프 치환 원칙과 아키텍처
객체지향의 초창기 리스코프 치환 원칙은 상속을 사용하도록 가이드하는 방법 정도로 간주되었다.
하지만 오늘날에 이르러 리스코프 치환 원칙은 인터페이스와 구현체에도 적용되는 좀 더 넓은 범위의 소프트웨어 설계 원칙으로 변화되었다.
여기서에서 말하는 인터페이스는 다양한 형태로 나타난다.
JVM 계열의 언어라면 인터페이스 하나와 이를 구현하는 여러 개의 클래스로 구성될 것이고,
루비라면 동일한 메서드 시그니처를 공유하는 여러 개의 클래스로 구성된다.
또는 동일한 REST 인터페이스에 응답하는 서비스 집단일 수도 있다.
위의 상황들은 물론 더 많은 경우에 리스코프 치환 원칙을 적용할 수 있다.
잘 정의된 인터페이스와 그 인터페이스의 구현체끼리의 상호 치환 가능성에 기대는 사용자들이 존재하기 때문이다.
아키텍처 관점에서 리스코프 치환 원칙를 이해하는 최선의 방법은 이 원칙을 어겼을 때 시스템 아키텍처에서 무슨 일이 일어나는지 관찰하는 것이다.
9.4. 결론
리스코프 치환 원칙은 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야한 한다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야할 수 있기 때문이다.