[개발서적] 클린 아키텍처 09 - LSP: 리스코프 치환 원칙
리스코프 치환 원칙이란?
리스코프 치환 원칙이란 상위 타입 클래스(S)를 상속받는 하위 타입 클래스(o1)를 다른 하위 타입 클래스(o2)로 치환하더라도 상위 타입 클래스(S)를 호출하는 프로그램(P)의 행위가 변하지 않아야 한다는 것을 뜻한다.
LSP를 준수하는 예시
위 다이어그램의 Billing 클래스는 License 클래스의 calcFee() 메서드를 호출하고 있다. 그리고 License는 PersonalLicense, BusinessLicense 두 가지 하위 타입을 가지고 있다. 두 하위 타입은 서로 다른 알고리즘을 이용해 라이선스 비용을 계산한다. Billing 클래스에서 어떤 하위 타입을 사용하더라도 calcFee() 메서드를 호출하는 행위는 변하지 않는다.
LSP를 위반하는 문제 (정사각형/직사각형 문제)
위 예제에서 Square는 Rectangle의 하위 타입으로 적합하지 않은데, Rectangle의 높이와 너비는 서로 독립적으로 변경될 수 있는 반면, Square의 높이와 너비는 반드시 함께 변경되기 때문이다. User는 Rectangle이라고 생각하여 함수를 호출하므로 혼동이 생길 수 있다.
Rectangle r = (a)
r.setW(5);
r.setH(2);
assert(r.area()==10);
위 코드에서 (a)를 Square로 생성한다면 assert문은 실패하게 된다. Square는 높이, 너비가 동일해야 하기 때문에 면적을 계산하는 방식도 다를 것이기 때문이다. 이런 형태의 LSP 위반을 막기 위해서는 Rectangle이 실제로 Square인지를 검사하는 메커니즘을 User에 추가하는 데 이러면 User의 행위가 사용하는 타입에 의존하게 되고, 결국 타입을 치환할 수 없게 된다.
LSP와 아키텍처
객체 지향이 등장한 초창기에는 LSP는 상속을 사용하도록 가이드하는 방법 정도 간주 되었다. 그러나 시간이 지나면서 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모해 왔다. 여기서 말하는 인터페이스는 다양한 형태로 나타난다.
아키텍처로 보는 LSP의 예
- 자바: 인터페이스 하나와 이를 구현하는 여러 클래스
- 루비: 메서드 시그니처를 공유하는 여러 개의 클래스
- REST 인터페이스에 응답하는 서비스 집단
아키텍처 관점에서 LSP 위배한 사례
택시 파견 서비스를 통합하는 애플리케이션의 예를 통해 아키텍처 관점에서 LSP 위반한 사례를 알아보자.
고객은 택시업체와 상관없이 자신의 상황에 가장 적합한 택시를 찾는다. 고객이 이용할 택시를 결정하면, 시스템은 REST 서비스를 통해 선택된 택시를 고객 위치로 파견한다. 서비스 사양서에 아래와 같은 필드를 추가하도록 되어있다고 가정한다.
REST URI | 요청 타입 |
/driver/%s/pickupAddress/%s/pickuptime/%s/destination/%s | put |
이때 한 택시업체 A에서 해당 사양서의 필드 중 destination 필드를 임의로 dest로 사용했다고 한다면, 해당 업체에 대한 예외사항을 처리하는 로직을 추가해야 할 것이다.
if (driver.getDispatchUri().startsWith("acme.com')) ...
이와 같은 업체들이 늘어나게 된다면 이러한 if 코드가 계속 증가하게 될 것이고, 이는 에러를 발생할 여지를 만들게 된다. 아키텍트는 이러한 버그로부터 시스템을 격리해야 한다.
결론
- LSP는 애플리케이션에서 호출하는 클래스의 하위 타입에 의존하지 않고 치환되더라도 애플리케이션의 행위는 변하지 않아야 한다.
- LSP는 아키텍처 수준까지 확장이 가능한 원칙이다.
- LSP를 위배하면 시스템 아키텍처가 오염되어 별도 메커니즘을 추가해야 한다.