Programming/Clean Architecture

9장. LSP: 리스코프 치환 원칙

JKROH 2023. 11. 29. 15:14
반응형

리스코프 치환 원칙

  • S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고, T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면, S는 T의 하위타입이다. 뭔 소리야
  • 쉽게 설명하면 다음과 같다. 프로그램 P에서 상위 타입 객체 T를 사용하고 있다면, T를 T의 하위타입 객체인 S로 대체해도 제대로 프로그램이 돌아가야한다.

상속을 사용하도록 가이드하기

  • 알림 전송 애플리케이션을 사용한다고 해보자.
  • 애플리케이션에서는 Notifier 객체를 사용한다.
  • 그런데, 카카오톡으로 알림을 보내야 할 때도 있고, 이메일로 보내야 할 때도 있고, 문자로 보내야 할 때도 있다.
  • 이 때, KakaoNotifier, EmailNotifier, SMSNotifier를 Notifier의 하위 객체로 설정하고 프로그램에서는 Notifier를 그대로 사용하게 해도 애플리케이션이 돌아가는 데에는 문제가 없다.
  • 즉 위 설계는 LSP를 준수한다. 애플리케이션의 행위가 Notifier의 하위타입 중 무엇을 사용하는지에 전혀 의존하지 않기 때문이다.

정사각형 / 직사각형 문제

  • 개념적으로 본다면 정사각형은 직사각형의 하위 타입이라고 볼 수 있다. 직사각형 중 가로 길이와 세로 길이가 같은 사각형을 우리는 정사각형이라고 부른다.
  • 그러나 프로그래밍 영역에서 직사각형(Rectangle)의 하위 타입 객체로 정사각형(Square)를 만드는게 적합할까?
  • Rectangle은 가로 길이와 세로 길이를 서로 다르게 변경할 수 있다. 그러나 Square는 둘을 다르게 변경할 수 없다. 즉, Rectangle의 행위를 Square가 완벽하게 대체할 수 없다.
  • 따라서, 위와 같이 설계하는 것은 LSP를 위반한다.

LSP와 아키텍처

  • 초기에는 LSP가 상속을 사용하도록 가이드하는 방법 정도로 간주되었다. 그러나 현재 LSP는 인터페이스와 구현체에도 적용되는 더 광범위한 소프트웨어 설계 원칙으로 변모해왔다.
  • 잘 정의된 인터페이스와 구현체끼리의 상호 치환 가능성에 기대는 것은 DIP, OCP 등의 원칙과 결부되어 좋은 코드를 작성하는 기반이 된다.

LSP 위배 사례

  • 우리 주변에는 수많은 중고 거래 애플리케이션이 존재한다. 우리는 이들을 하나로 통합하는 애플리케이션을 만들고자 한다.
  • 예를 들어 검색창에 '나성범 유니폼'을 검색하고 주문자의 정보 등을 URI에 담아 호출한다고 해보자.
    • application.com/name/'나성범유니폼'/phoneNumber/01011112222/address/'earth'
  • 이렇게 설정한다면, 우리 서비스에 통합된 모든 중고거래 애플리케이션은 위의 REST 인터페이스를 준수해야한다.
  • 그런데 만일 중고나라에서 새로 고용한 개발자들이 phoneNumber를 phone으로 줄여버린다면 어떻게 될까.
  • 우리는 이제 이런 예외 상황을 처리하는 코드를 따로 작성해서 넣어야한다. if(market.getName().equals("중고나라") 정도가 되겠다. 
  • 당연하지만 LSP를 위배하며, 모든 모듈에 위와 같은 코드를 추가하는 것은 추가적인 오류를 불러일으킬 수 있는 가능성을 내포하고있다.
  • 아키텍트는 추가적인 버그로부터 시스템을 격리해야 함은 물론 치환이 불가하게 하기 위한 추가적인 처리를 진행해야한다. 상당히 까다롭다.

결론

  • LSP는 아키텍처 수준까지 확장할 수 있고 확장해야 한다. 치환 가능성을 조금이라도 위배하면 시스템 아키텍처가 오염되어 상당량의 별도 메커니즘을 추가해야 할 수 있기 때문이다.
반응형