클린 아키텍처를 읽으며 정리한 내용입니다.
바바라 리스코프가 정의한 하위 타입이란 다음과 같다.
예시를 통해 리스코프 치환 원칙을 알아보자.
License
클래스는 calcFee()
라는 메서드를 가지며, Billing
애플리케이션에서 이 메서드를 호출한다고 가정해보자. License
를 구현한 하위 타입 PersonalLicense
와 BusinessLicense
는 서로 다른 알고리즘을 이용해 라이선스 비용을 계산한다.
이러한 상황에서 Billing
애플리케이션의 행위는 License
하위 타입 중 무엇을 사용하는지에 의존하지 않기 때문에, 이 하위 타입은 모두 License
타입을 치환할 수 있다. 따라서 이 설계는 LSP를 준수한다.
LSP를 위반하는 전형적인 문제로 정사각형/직사각형 문제가 있다.
Rectangle
클래스는 가로, 세로 길이를 변경하는 setH
, setW
메서드를 가지고 있다. 이 클래스의 하위 타입으로 Square
를 구현했다고 가정해보자. 이 예제에서 Square
의 높이와 너비는 반드시 함께 변경되어야 하기 때문에, Rectangle
의 하위 타입으로 적합하지 않다.
Rectangle
이 Square
인지 검사하는 매커니즘을 추가하는 등의 방법으로 해결할 수는 있겠지만, 이렇게 하면 user의 행위가 사용하는 타입에 의존하게 된다.
LSP가 나타난 초반에는 상속을 사용하도록 가이드하는 방법 정도로 간주되었지만, 시간이 지나며 인터페이스와 구현체에도 적용 가능한 원칙이 되었다.
여기에서 말하는 인터페이스란 다양한 형태로 나타날 수 있다.
아키텍처 관점에서 LSP를 이해하기 위해 이 원칙을 어기면 무슨 일이 일어나는지 알아보자.
다양한 택시 파견 서비스를 통합하는 애플리케이션의 예시를 보자.
택시 파견 REST 서비스의 URI가 운전기사 DB에 저장되어 있고, 시스템이 알맞은 기사를 선택해 URI로 다음과 같이 요청을 보낸다고 생각해보자.
purplecab.com/driver/Bob
/pickupAddress/24 Maple St.
/pickupTime/153
/destination/ORD
이 예시에서는 다양한 택시 파견 서비스가 동일한 REST 인터페이스를 준수하도록 만들어야 한다. 즉 다른 택시업체도 pickupAddress, pickupTime, destination 필드를 동일하게 처리해야 한다.
하지만 일부 택시업체에서 다르게 처리한다면, if문을 처리하는 등의 방법으로 해결해야 한다. 이런 예외상황은 끝없이 발생할 수 있다.
아키텍트는 이러한 버그로부터 시스템을 보호해야 한다. 파견 URI를 키로 사용해 파견 요청 포맷을 관리하거나, 각 REST 서비스의 인터페이스가 치환 가능하지 않다는 사실을 처리하는 복잡한 매커니즘을 추가해야 할 수도 있다.
LSP는 아키텍처 수준까지 확장할 수 있고, 위에서 본 문제를 막으려면 반드시 확장해야 한다.