하위 타입(Subtype)
S
타입의 객체o1
각각에 대응하는T
타입 객체o2
가 있고,T
타입을 이용해서 정의한 모든 프로그램P
에서o2
의 자리에o1
을 치환하더라도P
의 행위가 변하지 않는다면,S
는T
의 하위 타입이다.
상속을 사용하도록 가이드하기
classDiagram class Billing { } class License { << interface >> calcFee() } class PersonalLicense { } class BusinessLicense { users } Billing --> License License <|-- PersonalLicense License <|-- BusinessLicense
Billing
애플리케이션의 행위가 License
타입 중 무엇을 사용하는지에 전혀 의존하지 않이 때문에, 이들 하위 타입은 모두 License
타입을 치환할 수 있으므로 위 설계는 리스코프 치환 원칙을 준수한다.
정사각형/직사각형 문제
리스코프 치환 원칙을 위반하는 전형적인 문제로 유명한 정사각형/직사각형 문제가 있다.
classDiagram direction LR class User { } class Rectangle { H, W setH() setW() } class Square { setSide() } User --> Rectangle Rectangle <|-- Square
위 예제에서 Rectangle
의 높이와 너비는 서로 독립적으로 변경될 수 있는 반면, Square
의 높이와 너비는 반드시 함께 변경되므로 Square
는 Rectangle
의 하위 타입으로는 부적합하다.
이러한 경우 User
는 대화하고 있는 상대가 Rectangle
이라고 생각하므로 혼동이 생길 수 있다.
|
|
위와 같은 경우 ...
에 Square
를 생성한다면(치환한다면), assert
문은 실패하게된다.
이러한 형태의 리스코프 치환 원칙 위반을 막기 위한 유일한 방법은 검사하는 메커니즘을 User
에 추가하는 것 인데, User
의 행위가 사용하는 타입에 의존하게 되므로, 결국 타입을 서로 치환할 수 없게 된다.
LSP와 아키텍처
LSP는 상속을 사용하도록 가이드하는 방법 정도로 간주 되었으나, 시간이 지나면서 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모해왔다.
인터페이스
위에서 말하는 인터페이스는 여러 의미로 해석 가능하다.
- 인터페이스 하나와 이를 구현하는 여러 개의 클래스
- 동일한 메서드 시그니처를 공유하는 여러 개의 클래스
- 동일한 REST 인터페이스에 응답하는 서비스 집단
잘 정의된 인터페이스와 그 인터페이스의 구현체끼리의 상호 치환 가능성에 기대는 사용자들이 존재하기 때문에 대부분의 상황에서 LSP를 적용할 수 있다.
LSP 위배 사례: 택시 파견 서비스
요구사항
- 고객이 어느 택시업체인지는 신경쓰지 않고 자신의 상황에 가장 적합한 택시를 찾는다.
- 택시를 결정하면, 시스템은 REST 서비스를 통해 선택된 택시를 고객 위치로 파견한다.
- URI가 운전기사 데이터베이스에 저장되어 있다.
- URI 정보를 이용하여 해당 기사를 고객 위치로 파견한다.
- ex) Bob의 URI:
purplecab.com/driver/Bob
- 요청 예시
1 2 3 4
purplecab.com/driver/Bob /picupAddress/24 Maple St. /pickupTime/153 /destination/ORD
이러한 서비스를 만들 때 다양한 택시업체에서 동일한 REST 인터페이스를 반드시 준수하도록 만들어야한다.
만약 택시업체 ACME에서 destination
필드를 dest
로 축약해서 사용했다고 가정하면, 해당 예외 사항을 처리하는 로직을 추가해야만 한다.
|
|
- “acme"라는 단어를 코드 자체에 추가하면, 끔찍할 뿐만 아니라 이해할 수도 없는 온갖 종류의 에러가 발생할 여지를 만들게 된다.
- 새로운 택시업체 추가시 또 다른 if문이 필요할 수 있다.
- 위와 같은 버그를 방지하기 위해 설정용 데이터베이스를 이용하는 파견 명령 생성 모듈을 만들어야 할 수도 있다.
URI Dispatch Format Acme.com /pickupAddress/%s/pickupTime/%s/dest/%s *.* /pickupAddress/%s/pickupTime/%s/destination/%s - REST 서비스들의 인터페이스가 서로 치환 가능하지 않다는 사실을 처리하는 중요하고 복잡한 매커니즘을 추가해야 한다.
결론
LSP는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.