📖 [9장] LSP: 리스코프 치환 원칙

📘 클린 아키텍처 북스터디 정리입니다

📚 도서: 로버트 C. 마틴 《Clean Architecture》
🧑‍💻 목적: 올바른 설계에 대한 감각과 습관을 익히기 위해
🗓️ 진행 기간: 2025년 7월 ~ 매주 2장


✅ 핵심 요약 (Key Takeaways)

이 장의 핵심 문장은?

잘 정의된 인터페이스와 그 인터페이스의 구현체끼리의 상호 치환 가능성에 기대는 사용자들이 존재하기 때문이다

저자가 전달하고자 하는 메세지 요약

  • LSP는 단순한 상속 원칙이 아니라, 소프트웨어 시스템의 일관성과 유연성을 유지하기 위한 아키텍처 수준의 원칙

💡 내용 정리

서론

리스코프 치환 원칙(LSP)

  • 서브타입은 자신의 기반 타입(슈퍼타입)으로 교체할 수 있어야 한다 (바바라 리스코프, 1987)
  • 즉, 프로그램의 동작은 서브타입으로 치환되었을 때도 변하지 않아야 함

하위 타입의 정의

  • S 타입의 객체 o1 각각에 대응하는 T 타입 객체 o2가 있고,
  • T 타입을 이용해서 정의한 모든 프로그램 P에서 o2의 자리에 o1을 치환하더라도 P의 행위가 변하지 않는다면
  • S는 T의 하위타입이다

상속 기반 시스템에서 흔히 발생하는 문제

  • 하위 클래스가 상위 클래스의 기대와 다르게 동작하는 경우, 클라이언트 코드에서 instanceof, 타입 검사, 예외 처리가 필요해짐
    → 다형성 실패
  • 결과적으로 상속이 아니라 조건문 기반 의존 구조(if-else)가 됨

정사각형/직사각형 문제 - Bad case

  • Square가 Rectangle의 하위 타입처럼 보이지만, 동작이 다르므로 치환 불가능
  • LSP 위반을 막기 위해서 Rectangle이 실제로 Square인 지를 검사하는 메커니즘을 User에 추가해야 함
    -> User의 행위가 사용하는 타입의 의존하게 되므로 타입 치환 불가
class Rectangle {
    void setWidth(int w) { ... }
    void setHeight(int h) { ... }
    int getArea() { return width * height; }
}

class Square extends Rectangle {
    void setWidth(int w) {
        super.setWidth(w);
        super.setHeight(w);  // 너비 = 높이
    }

    void setHeight(int h) {
        super.setWidth(h);
        super.setHeight(h);
    }
}

Rectangle r = new Square();
r.setWidth(5);
r.setHeight(10);
System.out.println(r.getArea()); // 50을 기대했지만, 실제는 100

LSP - Good case

  • 클라이언트는 상위 타입인 License만 알면 되고, 하위 타입인PersonalLicneseBusinessLicense 중 어떤 객체가 와도 무관함
  • 클라이언트는 각 타입의 구체적 구현에 의존하지 않고, 다형성을 통해 동일한 행위 수행
public interface License {
	void calculateFee();
}

public class PersonalLicense implements License {
	pulbic void calculateFee(){
        // 개인 라이선스 요금 계산 로직
    }
}

public class BusinessLicense implements License {
	public void calculateFee(){
        // 비즈니스 라이선스 요금 계산 로직
    }
}

public class Billing {
    public void process(License license) {
        // License의 종류와 무관하게 동일한 인터페이스로 처리 가능
        license.calculateFee();
    }
}

LSP와 아키텍처

  • 객체 지향 초창기에 LSP는 상속을 사용하도록 가이드하는 방법 정도로 간주됨
  • 현재의 LSP는 광범위한 소프트웨어 설계 원칙
  • LSP 원칙 위반 시, 단순히 클래스 수준이 아닌 시스템 아키텍처 수준의 문제 발생

LSP 위배 사례

택시 파견 서비스 통합 앱

다형성 실패
  • 시스템은 모든 모든 URL에 대해 동일한 방식으로 URL을 조합해서 호출한다고 가정
  • Acme가 이 가정을 따르지 않음 → LSP 위배
클라이언트 코드 분기 처리 필요
  • 문제를 해결하기 위해 시스템에서 예외를 처리하는 분기를 해야 함
  • 결국 시스템은 각 업체의 세부 구현 사항을 알게 되고, 변경에 취약해짐
if (driver.getDispatchUri().startWith("acme.com"))...
OCP 위반
  • 새로운 업체를 추가하거나 사양 변경 시, 기존 시스템 수정이 필요할 수도 있음

결론

  • LSP는 아키텍처 수준까지 확장가능하고, 반드시 확장해야 함

💡 인상 깊었던 문장 & 나의 인사이트

책에서 가장 기억에 남는 문장

Billing 앱의 행위가 License 하위 타입 중 무엇을 사용하는 지에 전혀 의존하지 않기 때문이다.
이들 하위 타입은 모두 License 타입을 치환할 수 있다.

인상 깊었던 문장과 관련된 인사이트

LSP의 정의를 예시로 잘 설명한 문장이라고 생각된다.
특히 "치환할 수 있다"는 개념을 단순히 타입 호환의 문제가 아닌, 행위의 일관성이라는 관점에서 바라볼 수 있게 해주었다.

하위 타입이 상위 타입으로 치환될 수 있다는 말에서 가장 핵심적인 부분은, 클라이언트의 행위가 달라지지 않아야 한다는 것이다.
즉, Billing과 같은 클라이언트는 License의 하위 타입이 어떤 구현이든 관계없이 동일하게 작동해야 하며,
하위 타입이 추가되거나 변경되더라도 클라이언트 코드를 수정하지 않아도 되는 구조가 바람직한 설계다.

이번 장의 마지막 결론에서 저자가 강조했듯이, LSP는 단순한 클래스 수준의 원칙에 그치지 않고
전체 시스템의 유연성과 안정성을 좌우하는 아키텍처 설계 원칙으로 확장 가능하며, 반드시 그렇게 되어야 한다.


🛠 실무 적용 아이디어 (To Action)

✅ 오늘부터 실천할 작은 실천

  • 클라이언트가 구체 타입에 의존하지 않도록 인터페이스 중심으로 설계하도록 고민해보기

0개의 댓글