Liskov Substitution Principle

Liskov Substitution Principle(LSP, 리스코프 치환 원칙) 은 객체지향 설계의 SOLID 원칙 중 하나로, " 상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램의 동작에 문제가 없어야 한다 " 는 원칙이다. 이는 다형성 (polymorphism) 의 핵심 원칙이며, 올바른 상속 관계와 인터페이스 사용을 통해 시스템의 안정성과 예측 가능성을 확보하는 데 기여한다. 하위 클래스는 상위 클래스의 규약, 불변조건, 명세를 준수하여야 하며, 이를 위반할 경우 다형성과 상속의 이점이 사라지고 유지보수성, 확장성, 신뢰성이 떨어진다. LSP 는 상속을 올바르게 사용하고, 계약 기반 설계와 명세 준수를 통해 견고한 소프트웨어 구조를 만드는 데 필수적이다.

핵심 개념

리스코프 치환 원칙은 " 프로그램에서 부모 클래스의 인스턴스를 자식 클래스의 인스턴스로 치환해도 프로그램의 정확성에 영향을 미치지 않아야 한다 " 는 원칙이다.

기본 개념

심화 개념

배경

리스코프 치환 원칙은 1980 년대 중반 객체지향 프로그래밍이 발전하면서 상속의 올바른 활용에 대한 필요성이 대두되었다. 당시 “is-a” 관계만으로 상속을 결정하는 것이 충분하지 않음이 드러났고, Barbara Liskov 는 1987 년 OOPSLA 컨퍼런스에서 “Data abstraction and hierarchy” 라는 키노트 강연을 통해 이 문제를 해결할 새로운 개념을 제시했다.

목적 및 필요성

주요 목적:

필요성:

주요 기능 및 역할

  1. 행위적 호환성 보장: 상위 타입의 행위 규약을 하위 타입이 준수
  2. 타입 안전성 제공: 컴파일 타임과 런타임의 일관성 유지
  3. 다형성 지원: 안전한 객체 치환을 통한 다형적 동작
  4. 인터페이스 무결성 유지: 클라이언트 코드의 가정 보호

특징

핵심 원칙

graph TD
    A[리스코프 치환 원칙] --> B[행위적 서브타이핑 규칙]
    
    B --> C[사전조건 규칙]
    B --> D[사후조건 규칙]
    B --> E[불변식 규칙]
    B --> F[역사 제약 규칙]
    
    C --> C1[하위 타입은 사전조건을<br/>강화할 수 없음]
    C --> C2[더 관대한 입력 조건<br/>허용 가능]
    
    D --> D1[하위 타입은 사후조건을<br/>약화할 수 없음]
    D --> D2[더 강한 출력 보장<br/>제공 가능]
    
    E --> E1[클래스 불변식은<br/>항상 유지되어야 함]
    E --> E2[상속 후에도<br/>핵심 속성 보존]
    
    F --> F1[상위 타입에서 허용되지<br/>않는 상태 변경 금지]
    F --> F2[캡슐화를 통한<br/>상태 접근 제어]
    
    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#fff3e0
    style D fill:#e8f5e8
    style E fill:#fff8e1
    style F fill:#fce4ec
  1. 사전조건 약화 허용: 하위 타입은 상위 타입보다 더 관대한 입력을 받을 수 있음
  2. 사후조건 강화 허용: 하위 타입은 상위 타입보다 더 강한 보장을 제공할 수 있음
  3. 불변식 유지: 클래스의 핵심 속성은 상속 후에도 반드시 보존되어야 함
  4. 역사 제약 준수: 상위 타입에서 금지된 상태 변경은 하위 타입에서도 금지

계약 기반 설계 (Design by Contract)

  1. 계약 정의: 기본 클래스에서 사전조건, 사후조건, 불변식 명시
  2. 상속 구현: 하위 클래스에서 상위 클래스의 계약 준수
  3. 치환 실행: 런타임에 상위 타입 참조로 하위 타입 객체 사용
  4. 동작 검증: 클라이언트 코드의 가정과 기대 만족 확인
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
기본 클래스 계약:
┌─────────────────────────────────────┐
 require: precondition_base          
 ensure:  postcondition_base         
 invariant: invariant_base           
└─────────────────────────────────────┘
                    
            상속  오버라이드
                    
┌─────────────────────────────────────┐
 require: precondition_derived       
 ensure:  postcondition_derived        
 invariant: invariant_derived        
└─────────────────────────────────────┘

LSP 준수 조건:
- precondition_base  precondition_derived (약화 또는 동일)
- postcondition_base  postcondition_derived (강화 또는 동일)
- invariant_base = invariant_derived (동일하게 유지)

주요 원리 및 작동 원리

다이어그램

classDiagram
    class Rectangle {
      +setWidth()
      +setHeight()
      +getArea()
    }
    class Square {
      +setWidth()
      +setHeight()
      +getArea()
    }
    Square --|> Rectangle

Rectangle 객체를 기대하는 코드에 Square 객체를 넣었을 때, 동작이 달라지면 LSP 위반.

구조 및 아키텍처

필수 구성요소

구성 요소역할
상위 타입 (Base Class)공통 인터페이스 및 계약 정의
하위 타입 (Derived Class)상위 타입의 계약 준수 및 확장
계약 (Contract)입력 조건, 출력 조건, 예외 조건

선택 구성요소

구성 요소역할
어노테이션계약 검증 또는 문서화 (e.g. Python typing, Java annotations)
테스트 케이스하위 타입의 대체 가능성 검증

구조 및 아키텍처

graph TB
    subgraph "클라이언트 계층"
        C[클라이언트 코드]
        C --> I[인터페이스/추상 클래스]
    end
    
    subgraph "추상화 계층"
        I --> BC[기본 클래스 계약]
        BC --> PC[사전조건]
        BC --> POC[사후조건]
        BC --> INV[불변식]
    end
    
    subgraph "구현 계층"
        I --> S1[하위 타입 1]
        I --> S2[하위 타입 2]
        I --> S3[하위 타입 N]
        
        S1 --> SC1[하위 계약 1]
        S2 --> SC2[하위 계약 2]
        S3 --> SC3[하위 계약 N]
    end
    
    subgraph "검증 계층"
        SC1 --> V[LSP 검증기]
        SC2 --> V
        SC3 --> V
        
        V --> SR[시그니처 규칙]
        V --> CR[계약 규칙]
        V --> BR[행위 규칙]
        V --> HC[역사 제약]
    end
    
    style C fill:#e3f2fd
    style I fill:#f1f8e9
    style BC fill:#fff3e0
    style V fill:#fce4ec

LSP 의 아키텍처는 계층적 구조로 구성되며, 각 계층이 명확한 역할과 책임을 가진다. 클라이언트는 추상화 계층을 통해서만 구현 계층과 상호작용하며, 검증 계층이 LSP 준수를 보장한다.

구분구성 요소기능역할특징
필수기본 타입 (Base Type)계약 정의 및 기본 동작 명세하위 타입이 준수해야 할 인터페이스 제공사전조건, 사후조건, 불변식 명확화
하위 타입 (Subtype)기본 타입 계약 준수 및 특화 동작 구현치환 가능한 구현체 제공LSP 규칙에 따라 확장 기능 제공
계약 명세 (Contract Specification)타입 간 행위 규약 정의LSP 준수 여부 판단 기준 제공사전조건, 사후조건, 불변식으로 구성
클라이언트 코드 (Client Code)기본 타입의 계약에 따라 동작하위 타입과의 상호작용 수행타입과 무관하게 일관된 동작 기대
선택검증 도구 (Validation Tools)LSP 준수 여부 자동 검증개발 단계에서 위반 사항 탐지정적 분석 및 동적 테스트 모두 지원
문서화 시스템 (Documentation)계약 명세의 명시적 문서화개발자 간 계약 내용 공유명세 언어나 주석 기반으로 작성 가능
테스트 프레임워크 (Test Framework)치환 가능성 테스트 자동화회귀 테스트를 통해 LSP 준수 확인기본 타입과 하위 타입 간의 일치성 자동 검증 수행

구현 기법

구현 기법정의/구성목적/예시
계약 기반 설계상위 클래스의 사전조건, 사후조건, 불변조건 명시하위 클래스가 계약 위반하지 않도록 설계
명세 문서화상위 클래스의 기대 동작, 제약사항, 예외 등 문서화하위 클래스 설계 시 참고, LSP 위반 방지
상속 구조 재설계LSP 위반 시 상속 대신 조합 (Composition) 등 대안 적용Rectangle-Square 문제에서 별도 추상 클래스 (Shape) 로 분리

계약 기반 설계 (Design by Contract)

정의: 클래스와 메서드의 동작을 사전조건, 사후조건, 불변식으로 명시하는 기법
구성:

목적: LSP 준수를 위한 명확한 계약 정의
실제 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class BankAccount:
    def __init__(self, initial_balance: float):
        # 불변식: 잔액은 항상 0 이상이어야 함
        assert initial_balance >= 0
        self._balance = initial_balance
    
    def withdraw(self, amount: float) -> bool:
        # 사전조건: 출금액은 양수이고 잔액 이하여야 함
        assert amount > 0 and amount <= self._balance
        
        old_balance = self._balance
        self._balance -= amount
        
        # 사후조건: 잔액이 정확히 출금액만큼 감소
        assert self._balance == old_balance - amount
        assert self._balance >= 0  # 불변식 유지
        return True

class SavingsAccount(BankAccount):
    def withdraw(self, amount: float) -> bool:
        # 사전조건 약화: 더 관대한 조건 허용 (LSP 준수)
        if amount <= 0 or amount > self._balance:
            return False  # 예외 대신 False 반환으로 더 관대한 처리
        
        return super().withdraw(amount)

인터페이스 분리 (Interface Segregation)

정의: 클라이언트가 사용하지 않는 메서드에 의존하지 않도록 인터페이스를 분리
구성:

목적: LSP 위반을 방지하고 더 정확한 추상화 제공
실제 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from abc import ABC, abstractmethod

# LSP 위반 방지를 위한 인터페이스 분리
class Flyable(ABC):
    @abstractmethod
    def fly(self) -> None:
        pass

class Walkable(ABC):
    @abstractmethod
    def walk(self) -> None:
        pass

class Bird(Walkable):
    def walk(self) -> None:
        print("Bird is walking")

class FlyingBird(Bird, Flyable):
    def fly(self) -> None:
        print("Bird is flying")

class Penguin(Bird):
    # 펭귄은 날 수 없으므로 Flyable을 구현하지 않음
    pass

class Eagle(FlyingBird):
    def fly(self) -> None:
        print("Eagle soars high")

조합 패턴 (Composition Pattern)

정의: 상속 대신 조합을 통해 기능을 구현하여 LSP 위반을 방지
구성:

목적: 강한 결합을 피하고 유연한 설계 제공
실제 예시:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 상속 대신 조합을 사용한 LSP 준수 설계
class Shape:
    def __init__(self, area_calculator):
        self._area_calculator = area_calculator
    
    def get_area(self) -> float:
        return self._area_calculator.calculate()

class RectangleAreaCalculator:
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height
    
    def calculate(self) -> float:
        return self._width * self._height

class SquareAreaCalculator:
    def __init__(self, side: float):
        self._side = side
    
    def calculate(self) -> float:
        return self._side * self._side

# 사용
rectangle = Shape(RectangleAreaCalculator(5, 3))
square = Shape(SquareAreaCalculator(4))

장점과 단점

구분항목설명
✅ 장점다형성 안전성런타임 오류 없이 안전한 객체 치환 가능
코드 재사용성상위 타입을 위한 코드가 모든 하위 타입에 재사용 가능
유지보수성새로운 타입 추가 시 기존 코드 수정 불필요
확장성개방 - 폐쇄 원칙을 지원하여 시스템 확장 용이
신뢰성예측 가능한 동작으로 소프트웨어 신뢰성 향상
⚠ 단점설계 복잡성계약 명세와 검증 로직으로 인한 설계 복잡성 증가
성능 오버헤드런타임 검증과 추상화로 인한 성능 비용 발생
학습 곡선개발자가 LSP 개념과 적용 방법을 이해하는 데 시간 필요
과도한 추상화 위험불필요한 추상화로 인한 코드 복잡도 증가 가능성
도구 지원 부족대부분의 언어에서 LSP 검증을 위한 자동화 도구 부족

단점 해결 방법:

LSP 관련 도전 과제 및 해결책

도전 과제설명해결책
계약 명세의 어려움모든 가능한 동작을 명시적으로 정의하기 어려움핵심 계약에 집중하고 점진적 명세 개선, 의미 있는 상위 타입 정의
런타임 검증의 복잡성조건 검증을 모두 런타임에 수행하면 성능 부담개발 환경에서는 엄격히 검증, 운영 환경에서는 핵심 조건만 검증
상속 계층의 깊이 문제상속 구조가 깊어질수록 계약 준수 여부 확인이 복잡해짐인터페이스 기반 설계 및 조합 (Composition) 우선 원칙 적용으로 계층 단순화
레거시 코드 개선기존 코드에 LSP 를 적용하기 어려움점진적 리팩토링 및 래퍼 (Wrapper) 클래스 도입하여 단계적 이행
불완전한 추상화동작 의미가 다른 하위 타입이 상위 타입을 오용상위 타입의 의미를 명확히 하고, 의미상 불일치 시 계층 분리
테스트 자동화 어려움하위 타입이 상위 타입을 완전히 대체할 수 있는지 검증이 복잡함계약 기반 단위 테스트, 시나리오 테스트, 테스트 프레임워크 자동화 도입
상속 남용코드 재사용을 목적으로 불필요하거나 잘못된 상속 사용상속보다 조합 우선 적용, 상속은 is-a 관계일 때만 제한적으로 활용
Rectangle–Square 문제사각형과 정사각형 예처럼 하위 타입이 상위 타입의 계약을 위반하는 대표적 사례추상 클래스 분리 (예: Shape), 정사각형은 사각형의 하위 타입이 아닌 별도 조합으로 처리
계약 명세 관리사전조건, 사후조건, 불변식 등의 계약을 체계적으로 관리하기 어려움계약 기반 설계 (Design by Contract), 문서화 도구 활용

분류 기준에 따른 종류 및 유형

분류 기준종류설명
검증 방법정적 검증컴파일 타임에 타입 시스템을 통한 검증
동적 검증런타임에 assertion 과 테스트를 통한 검증
혼합 검증정적 검증과 동적 검증을 결합한 방식
계약 명시 방법명시적 계약코드에 직접 사전조건, 사후조건 작성
암시적 계약문서화나 주석을 통한 계약 명시
테스트 기반 계약단위 테스트를 통한 계약 표현
적용 범위메서드 레벨개별 메서드의 LSP 준수
클래스 레벨전체 클래스의 일관된 동작 보장
인터페이스 레벨인터페이스 구현체들 간의 치환 가능성
위반 처리 방식예외 발생LSP 위반 시 예외를 던져 실행 중단
기본값 반환예외 대신 안전한 기본값 반환
로깅 및 계속 실행위반 사항을 기록하고 실행 계속

실무 적용 예시

도메인/시스템적용 사례구현 방식 또는 기술 스택설명 / 효과
결제 시스템다양한 결제 수단 (신용카드, 페이 등) 처리PaymentProcessor 인터페이스 구현 (Java, Spring 등)새로운 결제 수단 추가 시 기존 코드 수정 없음
데이터베이스 접근다중 데이터소스 지원Repository 패턴으로 추상화 (Java, Python 등)DB 변경이 비즈니스 로직에 영향 미치지 않도록 보호
로깅 시스템다양한 로거 (Console, File 등) 지원Logger 인터페이스 사용로깅 방식 변경 시 구현 교체만으로 확장 가능
알림 시스템이메일, SMS, Push 등 멀티채널 알림NotificationSender 추상 클래스/전략 패턴 적용새로운 채널 추가 시 기존 로직 수정 없이 확장 가능
파일 처리 시스템다양한 파일 형식 (CSV, XML 등) 지원FileProcessor 인터페이스 구현각 파일 포맷별 처리기를 독립적으로 개발 가능
인증 시스템일반 사용자와 관리자 구분 인증AuthenticationProvider 인터페이스 (Node.js 등)인증 방식 확장 시 LSP 위반 없이 다형성 유지
캐시 시스템다양한 캐시 전략 (메모리, Redis 등) 적용CacheManager 인터페이스 또는 전략 패턴구현 교체 시 핵심 로직 유지, 테스트 용이
UI 컴포넌트버튼, 링크, 입력 등 공통 컴포넌트 구조화BaseComponent 또는 React 추상 컴포넌트일관된 이벤트 처리 방식 유지, 계약 위반 시 상속 대신 조합 사용 권장
금융 시스템계좌 타입 (일반 vs 신탁 등) 추상화Account 인터페이스 (Python, Django 등)하위 타입마다 출금 조건 다르면 계약 위반 가능 → 인터페이스 분리 필요
IoT 디바이스 제어다양한 센서, 액추에이터 통합 인터페이스 사용DeviceInterface (C++, Embedded 등)일부 디바이스가 공통 동작 미지원 시 → 공통 추상화 재설계 필요
도형 시스템 예시Shape, Rectangle, Square 구조 설계 예시추상 클래스 + 조합 기반 설계Square is not a Rectangle 문제 해결 위해 LSP 고려 설계 적용
프레임워크/API 설계상위 API 에 다형성으로 하위 구현체 연결Java Spring, Python, TypeScript 등하위 타입이 계약 위반 시 런타임 오류 발생 가능 → 명세 문서화 및 테스트 강화

활용 사례

사례 1: 결제 시스템

시나리오: 결제 시스템에서 PaymentMethod(상위 클래스), CreditCardPayment/PayPalPayment(하위 클래스) 존재.
구성: PaymentMethod 를 사용하는 코드가 하위 타입 (CreditCardPayment, PayPalPayment) 로 대체되어도 정상 동작해야 함.

시스템 다이어그램

classDiagram
    class PaymentMethod {
      +pay()
    }
    class CreditCardPayment {
      +pay()
    }
    class PayPalPayment {
      +pay()
    }
    CreditCardPayment --|> PaymentMethod
    PayPalPayment --|> PaymentMethod

Workflow:

  1. 결제 처리 코드가 PaymentMethod 타입 객체를 사용
  2. 하위 클래스 (CreditCardPayment, PayPalPayment) 로 대체해도 결제 처리 로직이 동일하게 동작
  3. 하위 클래스가 상위 클래스의 계약 (예: pay() 메서드 동작, 예외 등) 을 위반하면 LSP 위반

사례 2: 도형 면적 계산 시스템

문제 상황:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Rectangle:
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def set_width(self, w):
        self.width = w

    def set_height(self, h):
        self.height = h

    def area(self):
        return self.width * self.height

class Square(Rectangle):  # LSP 위반
    def set_width(self, w):
        self.width = w
        self.height = w

    def set_height(self, h):
        self.width = h
        self.height = h
1
2
3
4
def calculate_area(shape: Rectangle):
    shape.set_width(5)
    shape.set_height(10)
    print(shape.area())  # 예상: 50 → 실제: 100 (Square일 경우)

해결 방안:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Square(Shape):
    def __init__(self, length):
        self.length = length

    def area(self):
        return self.length ** 2

구조 다이어그램:

classDiagram
    class Shape {
        +area()
    }

    class Rectangle {
        +area()
    }

    class Square {
        +area()
    }

    Shape <|-- Rectangle
    Shape <|-- Square

사례 3: 전자상거래 주문 처리 시스템

시나리오: 온라인 쇼핑몰에서 다양한 주문 유형 (일반 주문, 예약 주문, 정기 주문) 을 처리하는 시스템

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
"""
전자상거래 주문 처리 시스템 - LSP 활용 사례
리스코프 치환 원칙을 적용한 주문 처리 시스템 구현
"""

from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from enum import Enum
from typing import List, Optional
import logging

class OrderStatus(Enum):
    PENDING = "pending"
    CONFIRMED = "confirmed"
    PROCESSING = "processing"
    SHIPPED = "shipped"
    DELIVERED = "delivered"
    CANCELLED = "cancelled"

class PaymentStatus(Enum):
    PENDING = "pending"
    COMPLETED = "completed"
    FAILED = "failed"

# LSP를 준수하는 기본 주문 클래스
class Order(ABC):
    """
    주문 기본 클래스 - LSP 계약 정의
    
    계약 조건:
    - 사전조건: 주문 ID는 양의 정수, 총액은 0 이상
    - 사후조건: 주문 처리 후 상태가 적절히 변경됨
    - 불변식: 주문 ID는 생성 후 변경 불가, 총액은 항상 0 이상
    """
    
    def __init__(self, order_id: int, customer_id: str, total_amount: float):
        # 사전조건 검증
        assert order_id > 0, "주문 ID는 양의 정수여야 합니다"
        assert total_amount >= 0, "총액은 0 이상이어야 합니다"
        
        self._order_id = order_id
        self._customer_id = customer_id
        self._total_amount = total_amount
        self._status = OrderStatus.PENDING
        self._payment_status = PaymentStatus.PENDING
        self._created_at = datetime.now()
        
        # 불변식 확인
        self._assert_invariants()
    
    def _assert_invariants(self):
        """클래스 불변식 검증"""
        assert self._order_id > 0, "주문 ID 불변식 위반"
        assert self._total_amount >= 0, "총액 불변식 위반"
    
    @property
    def order_id(self) -> int:
        return self._order_id
    
    @property
    def total_amount(self) -> float:
        return self._total_amount
    
    @property
    def status(self) -> OrderStatus:
        return self._status
    
    @abstractmethod
    def process_order(self) -> bool:
        """
        주문 처리 메서드
        
        사전조건: 주문 상태가 PENDING이어야 함
        사후조건: 성공 시 상태가 CONFIRMED 또는 PROCESSING으로 변경
        """
        pass
    
    @abstractmethod
    def calculate_shipping_cost(self) -> float:
        """
        배송비 계산
        
        사전조건: 총액이 0 이상이어야 함
        사후조건: 반환값은 0 이상이어야 함
        """
        pass
    
    def cancel_order(self) -> bool:
        """
        주문 취소
        
        사전조건: 주문이 SHIPPED 상태가 아니어야 함
        사후조건: 취소 성공 시 상태가 CANCELLED로 변경
        """
        # 사전조건 확인
        if self._status == OrderStatus.SHIPPED:
            return False
        
        old_status = self._status
        self._status = OrderStatus.CANCELLED
        
        # 사후조건 확인
        assert self._status == OrderStatus.CANCELLED, "취소 후 상태 변경 실패"
        
        self._assert_invariants()
        logging.info(f"주문 {self._order_id} 취소됨: {old_status} -> {self._status}")
        return True

# LSP를 준수하는 일반 주문 클래스
class RegularOrder(Order):
    """
    일반 주문 클래스
    기본 주문의 모든 계약을 준수하며 표준 처리 로직 구현
    """
    
    def __init__(self, order_id: int, customer_id: str, total_amount: float, 
                 shipping_address: str):
        super().__init__(order_id, customer_id, total_amount)
        self._shipping_address = shipping_address
    
    def process_order(self) -> bool:
        """
        일반 주문 처리
        LSP 준수: 기본 클래스의 계약을 모두 만족
        """
        # 사전조건 확인 (기본 클래스보다 약화되지 않음)
        if self._status != OrderStatus.PENDING:
            return False
        
        # 주문 처리 로직
        if self._validate_inventory() and self._process_payment():
            old_status = self._status
            self._status = OrderStatus.CONFIRMED
            
            # 사후조건 확인 (기본 클래스보다 강화됨)
            assert self._status == OrderStatus.CONFIRMED, "처리 후 상태 확인 실패"
            
            self._assert_invariants()
            logging.info(f"일반 주문 {self._order_id} 처리 완료: {old_status} -> {self._status}")
            return True
        
        return False
    
    def calculate_shipping_cost(self) -> float:
        """
        일반 주문 배송비 계산
        LSP 준수: 사전조건 약화, 사후조건 강화
        """
        # 사전조건 (기본 클래스와 동일하거나 약화)
        assert self._total_amount >= 0, "총액 사전조건 위반"
        
        # 배송비 계산 로직
        if self._total_amount >= 50000:  # 5만원 이상 무료배송
            shipping_cost = 0.0
        else:
            shipping_cost = 3000.0
        
        # 사후조건 (기본 클래스보다 강화)
        assert shipping_cost >= 0, "배송비 사후조건 위반"
        assert shipping_cost <= 3000, "최대 배송비 초과"  # 추가 보장
        
        return shipping_cost
    
    def _validate_inventory(self) -> bool:
        """재고 확인 (시뮬레이션)"""
        return True
    
    def _process_payment(self) -> bool:
        """결제 처리 (시뮬레이션)"""
        self._payment_status = PaymentStatus.COMPLETED
        return True

# LSP를 준수하는 예약 주문 클래스
class PreOrder(Order):
    """
    예약 주문 클래스
    출시 전 상품의 사전 주문을 처리
    """
    
    def __init__(self, order_id: int, customer_id: str, total_amount: float,
                 release_date: datetime):
        super().__init__(order_id, customer_id, total_amount)
        self._release_date = release_date
    
    def process_order(self) -> bool:
        """
        예약 주문 처리
        LSP 준수: 사전조건을 강화하지 않고, 사후조건을 약화하지 않음
        """
        # 사전조건 (기본 클래스와 동일)
        if self._status != OrderStatus.PENDING:
            return False
        
        # 예약 주문 특별 처리
        if self._validate_release_date() and self._reserve_item():
            old_status = self._status
            self._status = OrderStatus.CONFIRMED
            
            # 사후조건 (기본 클래스 계약 준수)
            assert self._status == OrderStatus.CONFIRMED, "처리 후 상태 확인 실패"
            
            self._assert_invariants()
            logging.info(f"예약 주문 {self._order_id} 처리 완료: {old_status} -> {self._status}")
            return True
        
        return False
    
    def calculate_shipping_cost(self) -> float:
        """
        예약 주문 배송비 계산
        LSP 준수: 출시일 기준으로 배송비 계산
        """
        # 사전조건 (기본 클래스와 동일)
        assert self._total_amount >= 0, "총액 사전조건 위반"
        
        # 예약 주문은 출시일에 무료배송 제공
        if self._release_date <= datetime.now() + timedelta(days=30):
            shipping_cost = 0.0  # 한 달 내 출시 시 무료배송
        else:
            shipping_cost = 2500.0  # 일반 배송비보다 할인
        
        # 사후조건 (기본 클래스 계약 준수)
        assert shipping_cost >= 0, "배송비 사후조건 위반"
        
        return shipping_cost
    
    def _validate_release_date(self) -> bool:
        """출시일 검증"""
        return self._release_date > datetime.now()
    
    def _reserve_item(self) -> bool:
        """상품 예약 (시뮬레이션)"""
        return True

# LSP를 준수하는 정기 주문 클래스
class SubscriptionOrder(Order):
    """
    정기 주문 클래스
    주기적으로 반복되는 주문을 처리
    """
    
    def __init__(self, order_id: int, customer_id: str, total_amount: float,
                 subscription_period: int):
        super().__init__(order_id, customer_id, total_amount)
        self._subscription_period = subscription_period  # 일 단위
        self._next_delivery = datetime.now() + timedelta(days=subscription_period)
    
    def process_order(self) -> bool:
        """
        정기 주문 처리
        LSP 준수: 기본 계약을 준수하며 정기 주문 특성 반영
        """
        # 사전조건 (기본 클래스와 동일)
        if self._status != OrderStatus.PENDING:
            return False
        
        # 정기 주문 처리
        if self._setup_subscription() and self._process_initial_payment():
            old_status = self._status
            self._status = OrderStatus.CONFIRMED
            
            # 사후조건 (기본 클래스 계약 준수)
            assert self._status == OrderStatus.CONFIRMED, "처리 후 상태 확인 실패"
            
            self._assert_invariants()
            logging.info(f"정기 주문 {self._order_id} 처리 완료: {old_status} -> {self._status}")
            return True
        
        return False
    
    def calculate_shipping_cost(self) -> float:
        """
        정기 주문 배송비 계산
        LSP 준수: 정기 주문 할인 혜택 적용
        """
        # 사전조건 (기본 클래스와 동일)
        assert self._total_amount >= 0, "총액 사전조건 위반"
        
        # 정기 주문은 항상 무료배송
        shipping_cost = 0.0
        
        # 사후조건 (기본 클래스 계약 준수)
        assert shipping_cost >= 0, "배송비 사후조건 위반"
        
        return shipping_cost
    
    def _setup_subscription(self) -> bool:
        """정기 주문 설정"""
        return True
    
    def _process_initial_payment(self) -> bool:
        """초기 결제 처리"""
        self._payment_status = PaymentStatus.COMPLETED
        return True

# 주문 처리 서비스 - LSP 덕분에 모든 주문 타입을 동일하게 처리
class OrderProcessingService:
    """
    주문 처리 서비스
    LSP 덕분에 주문 타입에 관계없이 일관된 처리 가능
    """
    
    def __init__(self):
        self._processed_orders: List[Order] = []
    
    def process_order(self, order: Order) -> bool:
        """
        주문 처리 - LSP 덕분에 모든 주문 타입을 동일하게 처리
        클라이언트 코드는 구체적인 주문 타입을 알 필요 없음
        """
        try:
            # 모든 Order 하위 클래스에서 동일하게 동작
            if order.process_order():
                self._processed_orders.append(order)
                
                # 배송비 계산도 일관되게 동작
                shipping_cost = order.calculate_shipping_cost()
                total_cost = order.total_amount + shipping_cost
                
                logging.info(f"주문 {order.order_id} 처리 완료 - "
                           f"상품비: {order.total_amount}, 배송비: {shipping_cost}, "
                           f"총액: {total_cost}")
                return True
            else:
                logging.warning(f"주문 {order.order_id} 처리 실패")
                return False
                
        except Exception as e:
            logging.error(f"주문 {order.order_id} 처리 중 오류: {e}")
            return False
    
    def cancel_order(self, order: Order) -> bool:
        """주문 취소 - 모든 주문 타입에서 일관된 동작"""
        return order.cancel_order()
    
    def get_order_summary(self, order: Order) -> dict:
        """주문 요약 정보 - LSP 덕분에 타입에 무관하게 동작"""
        return {
            "order_id": order.order_id,
            "total_amount": order.total_amount,
            "shipping_cost": order.calculate_shipping_cost(),
            "status": order.status.value,
            "order_type": order.__class__.__name__
        }

# 사용 예시 및 테스트
def demonstrate_lsp_compliance():
    """LSP 준수 시연"""
    
    # 다양한 주문 타입 생성
    orders = [
        RegularOrder(1001, "customer1", 45000, "서울시 강남구"),
        PreOrder(1002, "customer2", 75000, datetime.now() + timedelta(days=15)),
        SubscriptionOrder(1003, "customer3", 30000, 30)
    ]
    
    # 주문 처리 서비스
    service = OrderProcessingService()
    
    print("=== LSP 준수 시연: 다양한 주문 타입을 동일하게 처리 ===")
    
    # 모든 주문을 동일한 방식으로 처리 (LSP 덕분에 가능)
    for order in orders:
        print(f"\n--- {order.__class__.__name__} 처리 ---")
        
        # 주문 처리
        if service.process_order(order):
            # 주문 요약
            summary = service.get_order_summary(order)
            print(f"주문 요약: {summary}")
        else:
            print(f"주문 {order.order_id} 처리 실패")
    
    print("\n=== 모든 주문 타입에서 취소 기능 동작 확인 ===")
    
    # 모든 주문 타입에서 취소 기능이 일관되게 동작
    for order in orders:
        if service.cancel_order(order):
            print(f"{order.__class__.__name__} {order.order_id} 취소 성공")
        else:
            print(f"{order.__class__.__name__} {order.order_id} 취소 실패")

if __name__ == "__main__":
    # 로깅 설정
    logging.basicConfig(level=logging.INFO, 
                       format='%(asctime)s - %(levelname)s - %(message)s')
    
    # LSP 준수 시연
    demonstrate_lsp_compliance()

시스템 구성도:

graph TB
    subgraph "클라이언트 계층"
        WEB[웹 인터페이스]
        API[REST API]
        MOBILE[모바일 앱]
    end
    
    subgraph "서비스 계층"
        OPS[OrderProcessingService]
        OPS --> |LSP 준수로 일관된 처리| ORDER[Order 추상 클래스]
    end
    
    subgraph "도메인 계층 - LSP 적용"
        ORDER --> REG[RegularOrder]
        ORDER --> PRE[PreOrder] 
        ORDER --> SUB[SubscriptionOrder]
        
        REG --> |계약 준수| CONTRACT1[일반 주문 계약]
        PRE --> |계약 준수| CONTRACT2[예약 주문 계약]
        SUB --> |계약 준수| CONTRACT3[정기 주문 계약]
    end
    
    subgraph "인프라 계층"
        DB[(주문 데이터베이스)]
        PAYMENT[결제 게이트웨이]
        INVENTORY[재고 관리]
        NOTIFICATION[알림 서비스]
    end
    
    WEB --> OPS
    API --> OPS
    MOBILE --> OPS
    
    OPS --> DB
    OPS --> PAYMENT
    OPS --> INVENTORY
    OPS --> NOTIFICATION
    
    style ORDER fill:#e1f5fe
    style OPS fill:#f3e5f5
    style CONTRACT1 fill:#fff3e0
    style CONTRACT2 fill:#fff3e0
    style CONTRACT3 fill:#fff3e0

Workflow:

sequenceDiagram
    participant Client as 클라이언트
    participant Service as OrderProcessingService
    participant Order as Order (LSP)
    participant DB as 데이터베이스
    participant Payment as 결제 시스템
    
    Note over Client, Payment: LSP 덕분에 주문 타입에 무관하게 동일한 플로우
    
    Client->>Service: 주문 생성 요청
    Service->>Order: 주문 객체 생성 (RegularOrder/PreOrder/SubscriptionOrder)
    
    Note over Order: LSP 계약 검증
    Order->>Order: 사전조건 확인
    Order->>Order: 불변식 검증
    
    Service->>Order: process_order() 호출
    Order->>DB: 주문 정보 저장
    Order->>Payment: 결제 처리
    
    alt 결제 성공
        Payment-->>Order: 결제 완료
        Order->>Order: 상태 변경 (CONFIRMED)
        Order->>Order: 사후조건 확인
        Order-->>Service: 처리 성공
        Service-->>Client: 주문 처리 완료
    else 결제 실패
        Payment-->>Order: 결제 실패
        Order-->>Service: 처리 실패
        Service-->>Client: 주문 처리 실패
    end
    
    Note over Client, Payment: 배송비 계산도 동일한 인터페이스로 처리
    Service->>Order: calculate_shipping_cost() 호출
    Order-->>Service: 배송비 반환
    Service-->>Client: 총 비용 안내

LSP 의 역할:

  1. 일관된 처리: 모든 주문 타입을 동일한 인터페이스로 처리 가능
  2. 확장성: 새로운 주문 타입 추가 시 기존 코드 수정 불필요
  3. 안전성: 런타임에 예상치 못한 동작 방지
  4. 유지보수성: 타입별 특화 로직과 공통 로직의 명확한 분리

실무 적용 고려사항 및 권장사항

구분고려사항설명권장사항
설계 단계계약 명세의 명확성모호한 계약은 LSP 위반 가능성 증가사전조건, 사후조건, 불변식 등 구체적이고 측정 가능한 계약 정의
상속 계층의 깊이 제한깊은 상속 계층은 동작 추적과 LSP 검증을 어렵게 함3~4 단계 이내의 얕은 상속 계층 유지
인터페이스 최소화많은 메서드가 구현체에 불필요한 책임을 요구할 수 있음ISP (인터페이스 분리 원칙) 기반으로 작은 인터페이스 설계
계약 기반 설계상위 타입의 행위를 명확히 정의하지 않으면 LSP 검증 불가의미 기반 추상화와 계약 기반 설계 적용
구현 단계사전조건 강화 금지하위 클래스가 입력 조건을 더 엄격히 하면 클라이언트가 기대하는 행동을 깨뜨릴 수 있음상위 타입과 동일하거나 더 느슨한 입력 검증 유지
사후조건 약화 금지하위 클래스가 결과를 덜 보장하면 계약이 깨짐상위 타입과 동일하거나 더 강력한 출력 보장 유지
예외 처리 일관성 유지서로 다른 예외 처리 정책은 클라이언트 코드 오류 유발 가능동일한 예외 정책과 예외 타입을 유지
상속보다 조합 우선상속은 구현 강제 및 계약 위반 위험 존재컴포지션 및 위임 (Delegation) 우선 사용
테스트 단계치환 테스트 자동화모든 하위 타입이 상위 타입처럼 작동하는지 검증 필요매개변수화 테스트 및 테스트 팩토리 패턴 활용
계약 검증 테스트사전조건, 사후조건, 불변조건 충족 여부를 테스트Assertion 기반 단위 테스트 구성
단위 테스트 일관성하위 타입을 상위 타입 참조로 테스트할 수 있어야 함상위 타입 인터페이스 기반 테스트 설계
성능 테스트 포함다형성 구조가 성능에 미치는 영향을 확인할 필요 있음벤치마크 테스트 수행 및 성능 회귀 방지
유지보수 단계문서화 체계 구축계약 변경 시 문서와 코드가 불일치할 경우 오용 위험 존재명세 기반 문서화 및 버전 동기화 유지
리팩토링 안정성 확보LSP 위반 없이 구조를 개선하려면 검증 기반 개선이 필요테스트 기반 리팩토링 및 단계적 변경 전략 적용
하위 호환성 유지하위 클래스 변경이 기존 클라이언트 코드에 영향을 미치면 계약 위반 가능성 있음시맨틱 버저닝과 하위 호환 보장 정책 적용

최적화 고려사항 및 권장사항

구분고려사항설명권장사항
설계 최적화상위 타입 명확화추상화된 타입의 책임이 불명확할 경우 오용 발생최소 책임 단위 기준으로 추상화 정의
상속 남용 제한잘못된 상속 구조는 LSP 위반과 유지보수 난이도 상승 유발상속 최소화, 조합 (Composition) 및 위임 (Delegation) 우선 적용
계층 구조 단순화깊은 상속 구조는 유지보수 복잡성 및 계약 위반 가능성 증가3~4 단계 이내 유지, 역할 중심의 추상 계층화
역할 분리하위 클래스가 여러 역할을 가지면 테스트 및 확장 시 문제 발생SRP (단일 책임 원칙) 병행 적용
계약 문서화 및 표준화계약 조건이 명확하지 않으면 LSP 적용 및 검증 어려움사전/사후 조건 명시, 코드와 함께 명세화
성능 최적화가상 함수 호출 오버헤드다형성 구현 시 불필요한 추상화 계층은 성능 비용 유발핫스팟 구간에 대해 인라이닝 또는 구체 타입 직접 호출 적용
런타임 검증 비용과도한 assertion 은 운영 성능 저하 요인개발 환경과 운영 환경에 따라 검증 강도 조절
메모리 사용량 관리상속 계층이 깊을수록 객체 수 증가로 메모리 부담 발생경량 객체 구성, 메모리 풀, 불변 객체 설계
불필요한 추상화 제거필요 없는 추상화는 복잡성만 증가시키고 성능 저하YAGNI(You Aren’t Gonna Need It) 원칙 적용
정적 다형성 활용컴파일 타임 결정 가능한 추상화는 런타임 비용 절감제네릭 (Generic), 템플릿 (Template) 기반 정적 바인딩 설계 적용
코드 품질테스트 자동화하위 타입이 대체 가능함을 검증하는 테스트 필수파라미터화 테스트 (@pytest.mark.parametrize 등) 사용
계약 기반 테스트계약의 사전/사후조건, 불변식 충족 여부 확인 필요Assertion 기반 단위 테스트와 시나리오 테스트 구성
테스트 분리각 하위 타입의 역할과 책임에 따라 개별 테스트 필요인터페이스 기반 테스트 구성, Mock/Stubs 분리 사용
아키텍처 최적화계층 분리 설계지나치게 단일 계층에 모든 책임 집중 시 확장성과 유지보수성 저하도메인별 계층 설계, Adapter/Port 기반 구조 분리 적용
캐싱 전략 적용동일 동작 반복 수행 시 불필요한 처리 비용 발생결과 캐싱 (Memoization), 불변 객체 설계 활용
배치/대용량 처리 전략대량 데이터 처리 시에도 계약을 지키며 성능 저하를 피해야 함병렬 처리 (Parallelism), 스트리밍 (Stream Processing) 기반 처리 적용

LSP 관련 문제와 해결 방안

문제 유형원인영향탐지/진단예방 방안해결 방안
비정상 상속하위 클래스가 상위 클래스의 계약 (사전/사후 조건 등) 을 위반예외 발생, 동작 불일치다형성 테스트, 계약 기반 테스트의미 있는 추상 타입 정의, 명확한 계약 명세인터페이스 분리, 조합 (Composition) 전환
사전조건 강화하위 클래스가 더 많은 입력 조건을 요구예외 발생, 비정상 상태 유발조건 불일치 테스트사전조건 동일하게 유지, 공통 입력 검증 적용계약 기반 테스트 도입, 하위 클래스 입력 조건 완화
사후조건 약화하위 클래스가 결과 보장을 축소결과 신뢰도 저하, 테스트 실패Assertion 기반 테스트출력 조건 일관성 유지, 상위 클래스와 동일한 출력 범위 제공출력 결과 보장 강화 또는 상위 계약 준수
불완전 다형성일부 하위 클래스만 정상 작동하거나 일부 기능을 무시다형성 붕괴, 예외 발생파라미터화 테스트, Stub/Mock 테스트 실패하위 타입별 계약 테스트, 추상화 수준 점검클래스 계층 재구조화, 필요 시 특정 구현 제거
빈 메서드 오버라이드불필요한 상속, 넓은 인터페이스 설계로 인한 비정상적인 메서드 오버라이딩의도와 다른 무효 동작, 디버깅 어려움코드 리뷰, 동작 테스트능력별 인터페이스 분리 (ISP), 불필요한 상속 제거조합 기반 설계로 전환, 기능 단위 분리 설계
예외 처리 불일치하위 클래스에서 상위 클래스와 다른 예외를 발생시키거나 계약이 없음예외 처리 실패, 시스템 안정성 저하테스트 실패, 예외 패턴 분석예외 계약 명시화, 공통 예외 정책 정의공통 예외 형식으로 변환, 예외 캡슐화 처리
성능 특성 불일치알고리즘 복잡도 상이, 외부 의존성 차이, 캐싱 미적용응답 지연, 전체 시스템 성능 저하, UX 저하벤치마크 테스트, 시간 복잡도 분석성능 계약 명시, 알고리즘 기준 정의, 리소스 사용 명확화캐싱 도입, 성능 최적화, O(n) 수준으로 보장
상속 남용재사용 또는 계층 구조 오해로 인한 과도한 상속 사용테스트 복잡성 증가, 코드 재사용성 저하상속 계층 수, 클래스 간 결합도 분석조합 우선 설계, 상속 깊이 제한조합/위임 패턴 도입, 추상 클래스 분리
Rectangle-Square 문제도형의 동작 규칙 불일치 (너비/높이 독립 vs 동일)테스트 실패, 치환 불가, 비정상 상태 발생get_area 테스트, set_width/set_height 행동 비교도형 추상 클래스 분리, 동작 기반 인터페이스 사용별도 타입 정의 (Rectangle ≠ Square), 조합 패턴 활용

Rectangle-Square 문제

문제: 수학적으로 정사각형은 직사각형이지만, 코드에서는 LSP 위반을 야기
원인:

영향:

탐지 및 진단:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 문제 탐지 테스트
def test_rectangle_area_calculation():
    rectangle = Rectangle(5, 3)
    rectangle.set_width(4)
    assert rectangle.get_area() == 12  # 4 * 3 = 12 예상
    
    # Square가 Rectangle을 상속하면 실패
    square = Square(3)  # Rectangle을 상속한 경우
    square.set_width(4)
    assert square.get_area() == 12  # 실제로는 16이 됨 (4 * 4)

예방 방법:

해결 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 해결책 1: 인터페이스 분리
class Shape(ABC):
    @abstractmethod
    def get_area(self) -> float:
        pass

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height
    
    def set_width(self, width: float):
        self._width = width
    
    def set_height(self, height: float):
        self._height = height
    
    def get_area(self) -> float:
        return self._width * self._height

class Square(Shape):
    def __init__(self, side: float):
        self._side = side
    
    def set_side(self, side: float):
        self._side = side
    
    def get_area(self) -> float:
        return self._side * self._side

빈 메서드 오버라이드 문제

문제: 하위 클래스에서 상위 클래스 메서드를 빈 구현으로 오버라이드
원인:

영향:

탐지 및 진단:

1
2
3
4
5
6
7
8
# 문제가 있는 코드 패턴
class Bird:
    def fly(self):
        print("Flying")

class Penguin(Bird):
    def fly(self):
        pass  # 빈 구현 - LSP 위반 신호

예방 방법:

해결 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 해결책: 능력별 인터페이스 분리
class Animal:
    def eat(self):
        print("Eating")

class Flyable:
    def fly(self):
        print("Flying")

class Swimmable:
    def swim(self):
        print("Swimming")

class Eagle(Animal, Flyable):
    pass

class Penguin(Animal, Swimmable):
    pass

예외 처리 불일치 문제

문제: 하위 클래스에서 상위 클래스와 다른 예외 발생
원인:

탐지 및 진단:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 문제 탐지
class FileProcessor:
    def process(self, filename: str) -> str:
        if not filename:
            raise ValueError("Filename required")
        return f"Processed {filename}"

class DatabaseFileProcessor(FileProcessor):
    def process(self, filename: str) -> str:
        if not filename:
            raise ValueError("Filename required")
        if not self._db_connection:
            raise ConnectionError("Database unavailable")  # 새로운 예외 타입
        return f"Processed {filename} in database"

예방 방법:

해결 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 해결책: 예외 계약 준수
class FileProcessor:
    def process(self, filename: str) -> str:
        if not filename:
            raise ValueError("Filename required")
        return self._do_process(filename)
    
    def _do_process(self, filename: str) -> str:
        return f"Processed {filename}"

class DatabaseFileProcessor(FileProcessor):
    def _do_process(self, filename: str) -> str:
        try:
            if not self._db_connection:
                # 내부적으로 처리하고 기본 예외로 변환
                raise ValueError("Processing unavailable")
            return f"Processed {filename} in database"
        except ConnectionError:
            # 예상 가능한 예외를 기본 예외로 변환
            raise ValueError("Processing failed")

성능 특성 불일치 문제

문제: 하위 클래스에서 상위 클래스와 현저히 다른 성능 특성
원인:

탐지 및 진단:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time
from abc import ABC, abstractmethod

class DataProcessor(ABC):
    @abstractmethod
    def process_data(self, data: list) -> list:
        """Expected O(n) complexity"""
        pass

class QuickProcessor(DataProcessor):
    def process_data(self, data: list) -> list:
        # O(n) 복잡도
        return [x * 2 for x in data]

class SlowProcessor(DataProcessor):
    def process_data(self, data: list) -> list:
        # O(n²) 복잡도 - 성능 특성 위반
        result = []
        for i in data:
            for j in data:  # 불필요한 중첩 루프
                if i == j:
                    result.append(i * 2)
                    break
        return result

예방 방법:

해결 방법:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 해결책: 성능 계약 준수와 최적화
class OptimizedSlowProcessor(DataProcessor):
    def __init__(self):
        self._cache = {}
    
    def process_data(self, data: list) -> list:
        # 캐싱으로 O(n) 복잡도 달성
        cache_key = hash(tuple(data))
        if cache_key not in self._cache:
            self._cache[cache_key] = [x * 2 for x in data]
        return self._cache[cache_key]

주제와 관련하여 주목할 내용

주제 분야항목설명
설계 원칙LSP (리스코프 치환 원칙)상위 타입 객체를 하위 타입으로 대체해도 정확성과 기능이 보장되어야 함
계약 기반 설계 (Design by Contract)사전조건, 사후조건, 불변식 준수를 통한 하위 타입의 행동 보장
컴포지션 vs 상속상속보다 조합 (Composition) 우선 사용을 통해 유연성과 LSP 준수 강화
테스트 전략대체 가능성 검증 테스트단위 테스트, 시나리오 테스트, 파라미터화 테스트 등을 활용해 하위 타입의 대체 가능성 검증
계약 기반 테스트클래스의 계약 조건 (입력/출력/예외 등) 에 대한 자동화 테스트를 통한 위반 탐지
프로퍼티 기반 테스트다양한 무작위 입력에 대한 기대 동작 검증으로 LSP 위반을 조기에 탐지
리팩토링 전략Replace Inheritance with Delegation상속 구조가 LSP 위반 시 조합 방식으로 안전하게 리팩토링하는 패턴
고급 타입 시스템제네릭과 공변성/반공변성제네릭 타입에서의 치환 가능성과 타입 안전성 확보 관련 설계 원칙
타입 추론과 치환성컴파일러의 타입 추론이 계약 일치 여부에 어떤 영향을 주는지 분석
함수형 프로그래밍함수 서브타이핑고차 함수에서 파라미터/반환 타입의 치환 가능성 규칙 준수
모나드와 행위적 서브타이핑모나드 기반 연산에서 LSP 와 유사한 행위 보장 요구
동시성 프로그래밍스레드 안전성멀티스레드 환경에서도 하위 타입이 상위 타입과 동일한 스레드 안정성 제공
비동기/async 구조에서의 치환성async/await 흐름에서도 동일한 인터페이스 및 동작 보장 필요
마이크로서비스 설계서비스 인터페이스 설계API 버전 변경이나 구현체 교체 시에도 외부 클라이언트가 문제없이 작동해야 함
계약 테스트 (Contract Test)Consumer Driven Contract 로 서비스 간의 계약 유지 여부 검증
도메인 주도 설계 (DDD)도메인 모델링엔티티 및 밸류 오브젝트가 상위 도메인 모델의 행위를 충실히 따르도록 설계
애그리게이트 루트와 LSP 적용애그리게이트 내부에서 하위 타입이 루트 계약을 위반하지 않도록 제한
성능 엔지니어링성능 최적화와 일관성 유지하위 타입의 성능 특성이 상위 타입의 기대 성능을 충족해야 함
메모리 효율성객체 계층 구조에서 과도한 자원 사용 방지, 불필요한 상속 제거
정적 분석 및 도구 활용타입 체커 지원TypeScript, MyPy 등에서 LSP 위반 탐지를 위한 정적 분석 지원
LSP 자동 검증 도구계약 분석 기반의 정적 코드 검사 도구 사용으로 위반 조기 탐지 가능

추가 학습 필요 내용

분류주제설명
설계 원칙Design by Contract사전조건/사후조건/불변식을 기반으로 한 계약 중심 설계 방식
SOLID 통합 설계OCP, SRP, ISP 등과의 연계 설계 원칙
Polymorphism인터페이스 기반 다형성과 LSP 관계 이해
Interface 설계와 분리ISP(인터페이스 분리 원칙) 과 함께 적용해 인터페이스 최소화
Replace Inheritance with Delegation상속 남용 구조 리팩토링을 위한 대표적 패턴
테스트 전략Substitutability 테스트하위 클래스가 상위 클래스와 동일하게 동작하는지 확인
계약 기반 테스트계약 조건에 맞는 행동 검증 (입출력, 예외 등)
Mock/Stub 기반 테스트대체 가능한 구현체 테스트 전략
모델 기반 테스트상태 기반 시스템에서의 LSP 검증
퍼즈 테스트 (Fuzz Testing)예측 불가능한 입력으로 LSP 위반 탐지
돌연변이 테스트 (Mutation Testing)테스트의 견고함과 LSP 위반 검출 효과 분석
아키텍처/실무 적용헥사고날/클린 아키텍처경계 (포트/어댑터) 에서 LSP 준수를 통한 확장성 확보
Repository 패턴데이터 계층에서 대체 가능한 구현 설계
전략/데코레이터/상태 패턴과 LSP동작 변경 가능성과 행위 일치성 보장
서비스 인터페이스 설계마이크로서비스에서 API 버전과 하위 호환성 보장
Consumer Driven Contract서비스 간 계약 테스트 방식
플랫폼 추상화 및 MVVM 구조모바일 개발에서 LSP 적용 방식
미들웨어 체인 구조 설계Express.js 등에서 하위 미들웨어 교체 가능성 확보
레거시 코드 리팩토링점진적 LSP 적용과 래퍼 (Wrapper) 패턴 사용
API 설계 전략REST/GraphQL API 설계 시의 LSP 고려사항
언어별 적용 기법Java, C#, Python, TypeScript각 언어별로 LSP 적용 방식과 타입 시스템 특성 이해
Duck TypingPython 등 동적 언어에서의 치환 가능성 판단 기준
제네릭과 타입 시스템공변성/반공변성, 타입 추론 등에서의 LSP 문제 분석
도메인 특화 적용도메인 모델링과 애그리게이트 루트DDD 관점에서 계약 일치가 중요한 모델 구조 설계
ORM 상속 전략객체 - 관계 매핑 시 LSP 준수와 상속 전략 설계
ECS 구조게임 개발에서 교체 가능한 컴포넌트 모델 설계
AI 행동 트리AI 에서의 노드 대체 가능성과 LSP 관계
이론/검증 기법형식 검증 (Formal Verification)수학적 모델을 통한 LSP 위반 검증
프로그램 의미론 (Semantics)LSP 의 의미론적 해석과 추론 기반 설계
계약 기반 언어 및 도구Eiffel, Spec#, Dafny, JML, PyContracts
정적 분석 및 타입 체커LSP 자동 검출 도구: TypeScript, MyPy, SonarQube 등
성능/운영 고려사항성능 특성의 일관성 유지하위 클래스에서의 성능 저하 예방 (복잡도, 캐싱 등)
메모리 및 리소스 사용 최적화객체 계층 구조에서 불필요한 자원 낭비 방지
테스트 자동화와 지속적 통합CI/CD 파이프라인에서 치환 가능성 검증 포함

용어 정리

핵심 개념

용어설명
Liskov Substitution Principle (LSP)하위 타입이 상위 타입을 완전히 대체할 수 있어야 한다는 객체지향 원칙
치환 가능성 (Substitutability)상위 클래스 참조를 하위 클래스 인스턴스로 바꾸어도 동작이 유지되는 성질
행위적 서브타이핑 (Behavioral Subtyping)구조가 아닌 행동의 일치를 중심으로 한 서브타이핑 개념
강한 행위적 서브타이핑 (Strong Behavioral Subtyping)LSP 를 완벽히 만족시키는 의미적 치환 가능성을 강조한 개념

계약 설계 요소 (Design by Contract)

용어설명
계약 기반 설계 (Design by Contract)사전조건, 사후조건, 불변조건 등을 명시하고 이를 기반으로 클래스의 행위를 정의하는 설계 방법
사전조건 (Precondition)메서드 호출 전에 반드시 만족해야 하는 조건
사후조건 (Postcondition)메서드 실행 후 반드시 보장해야 하는 결과 조건
불변조건 / 불변식 (Invariant)객체 생애주기 동안 항상 유지되어야 하는 속성
역사 제약 (History Constraint)객체의 상태 변화 이력을 제한하는 조건 (예: 상태 되돌리기 불가 등)

타입 시스템

용어설명
공변성 (Covariance)하위 타입이 상위 타입의 자리에 올 수 있는 특성 (예: List[Dog]List[Animal]?)
반공변성 (Contravariance)상위 타입이 하위 타입의 자리에 올 수 있는 특성 (주로 함수의 파라미터에서 적용됨)
무공변성 (Invariance)타입 매개변수가 완전히 동일해야만 타입이 호환되는 성질

설계 원칙

용어설명
SOLID 원칙객체지향 설계의 5 대 원칙 집합 (SRP, OCP, LSP, ISP, DIP)
개방 - 폐쇄 원칙 (Open-Closed Principle)소프트웨어는 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 설계 원칙
인터페이스 분리 원칙 (Interface Segregation Principle)클라이언트는 자신이 사용하지 않는 메서드에 의존하지 않아야 한다는 원칙

설계 기법

용어설명
다형성 (Polymorphism)동일한 상위 타입 인터페이스로 다양한 하위 타입을 처리할 수 있는 객체지향 특성
조합 (Composition)객체를 포함 (위임) 하여 기능을 구성하는 방식, 상속보다 유연한 구조 설계 가능
다형성 테스트다양한 하위 타입이 상위 타입으로서 정상 작동하는지 검증하는 테스트 전략

참고 및 출처

학술 논문 및 서적

공식 문서 및 온라인 자료

기술 블로그 및 커뮤니티

영상 자료