하위 타입(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 4purplecab.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는 아키텍처 수준까지 확장할 수 있고, 반드시 확장해야만 한다.
치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.
