객체 지향 프로그래밍(OOP)에서 리스코프 치환 원칙(Liskov Substitution Principle, LSP)의 예시

2024-07-27

리스코프 치환 원칙은 객체 지향 프로그래밍의 중요한 설계 원칙 중 하나이며, 상위 타입의 객체를 하위 타입의 객체로 안전하게 대체할 수 있어야 한다는 것을 의미합니다. 즉, 프로그램 코드에서 상위 타입을 사용하는 부분에서 하위 타입 객체를 사용해도 프로그램 동작에 문제가 발생해서는 안 된다는 것입니다.

예시

다음은 리스코프 치환 원칙 위반 예시입니다.

class Vehicle:
  def drive(self):
    pass

class Car(Vehicle):
  def drive(self):
    print("Driving a car")

class Truck(Vehicle):
  def drive(self):
    print("Driving a truck")

def main():
  # 상위 타입인 Vehicle 객체를 생성합니다.
  vehicle = Vehicle()

  # vehicle 객체를 사용하여 drive() 메서드를 호출합니다.
  vehicle.drive()  # 출력: Driving a car

  # vehicle 객체를 Car 객체로 대체합니다.
  vehicle = Car()

  # vehicle 객체를 사용하여 drive() 메서드를 호출합니다.
  vehicle.drive()  # 출력: Driving a car (정상 동작)

  # vehicle 객체를 Truck 객체로 대체합니다.
  vehicle = Truck()

  # vehicle 객체를 사용하여 drive() 메서드를 호출합니다.
  vehicle.drive()  # 예외 발생!

if __name__ == "__main__":
  main()

위 예시에서 Truck 클래스는 Vehicle 클래스의 하위 클래스입니다. 하지만 Truck 객체의 drive() 메서드는 Vehicle 객체의 drive() 메서드와 동일한 동작을 보장하지 않습니다. 즉, Vehicle 객체를 Truck 객체로 대체하면 예외가 발생하여 프로그램이 정상 작동하지 않습니다. 이는 리스코프 치환 원칙을 위반하는 예시입니다.

올바른 설계

리스코프 치환 원칙을 위반하지 않도록 하려면 Truck 클래스의 drive() 메서드를 다음과 같이 설계해야 합니다.

class Truck(Vehicle):
  def drive(self):
    print("Driving a truck with a trailer")

이렇게 수정하면 Truck 객체의 drive() 메서드는 Vehicle 객체의 drive() 메서드와 동일한 기본 동작을 제공하면서도 추가적인 기능을 수행합니다. 즉, Vehicle 객체를 Truck 객체로 대체해도 프로그램은 정상 작동하며, 리스코프 치환 원칙을 만족합니다.

SOLID 원칙과의 관계

리스코프 치환 원칙은 객체 지향 프로그래밍의 설계 원칙 중 하나인 SOLID 원칙 중 **O(Open-Closed Principle)**와 밀접하게 관련됩니다. OCP는 "클래스는 개방되어야 하지만, 수정은 닫혀 있어야 한다"는 것을 의미하며, 이는 리스코프 치환 원칙을 통해 구현될 수 있습니다. 즉, 기존 코드를 변경하지 않고도 하위 클래스를 추가하거나 수정할 수 있어야 하며, 이를 통해 코드의 유지보수성을 높일 수 있습니다.




리스코프 치환 원칙을 따르는 예시 코드

class Shape:
  def draw(self):
    pass

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

  def draw(self):
    print(f"사각형을 그립니다. 폭: {self.width}, 높이: {self.height}")

class Square(Rectangle):
  def __init__(self, side_length):
    super().__init__(side_length, side_length)

def main():
  # 상위 타입인 Shape 객체를 생성합니다.
  shapes = [
    Rectangle(5, 3),
    Square(4),
  ]

  # 각 Shape 객체의 draw() 메서드를 호출합니다.
  for shape in shapes:
    shape.draw()

if __name__ == "__main__":
  main()

위 예시에서 Square 클래스는 Rectangle 클래스의 하위 클래스입니다.

  • Shape 클래스는 추상 클래스이며 draw() 메서드를 정의하지 않습니다.
  • Rectangle 클래스는 Shape 클래스의 하위 클래스이며 draw() 메서드를 구현하여 사각형을 그리는 방법을 정의합니다.

main() 함수에서는 Shape 객체의 목록을 생성하고 각 객체의 draw() 메서드를 호출합니다. Square 객체는 Rectangle 객체로 대체되었지만, 프로그램은 정상적으로 작동하며 각 객체의 종류에 맞는 도형을 그립니다.

이 예시는 리스코프 치환 원칙을 만족하는 코드입니다. 왜냐하면:

  • Square 객체는 Rectangle 객체의 모든 기능을 제공합니다.
  • Square 객체는 Rectangle 객체와 동일한 기본 동작을 제공하면서도 추가적인 기능을 수행합니다 (정사각형임을 명시적으로 표시).
  • Shape 객체를 Square 객체로 대체해도 프로그램은 정상 작동합니다.



리스코프 치환 원칙 위반을 방지하는 대체 방법

상속 구조 개선

  • 추상 클래스 활용: 공통적인 속성과 메서드를 정의하는 추상 클래스를 사용하여 하위 클래스 간의 계층 구조를 명확하게 정의합니다.
  • 인터페이스 활용: 서로 다른 하위 클래스에서 구현해야 하는 공통적인 기능을 정의하는 인터페이스를 사용합니다.
  • 다중 상속 활용: 하나의 상위 클래스 대신 여러 상위 클래스로부터 상속받아 특화된 기능을 제공합니다.

메서드 오버라이딩 주의

  • 하위 클래스 메서드는 상위 클래스 메서드의 기본 동작을 유지하면서 추가적인 기능을 제공해야 합니다.
  • **하위 클래스 메서드의 반환 값 유형은 상위 클래스 메서드의 반환 값 유형과 호환되어야 합니다.
  • 하위 클래스 메서드에서 예외를 throw하는 경우, 상위 클래스 메서드에서 throw하는 예외의 하위 유형이어야 합니다.

객체 생성 및 사용 주의

  • 적절한 타입 변수 사용: 객체를 생성하고 사용할 때 올바른 타입 변수를 사용하여 컴파일러 오류를 방지하고 명확한 코드 작성을 유지합니다.
  • ** instanceof 연산자 활용:** 객체가 특정 클래스의 인스턴스인지 확인하기 위해 instanceof 연산자를 사용하여 안전하게 코드를 작성합니다.
  • 클래스 캐스팅 주의: 객체를 다른 타입으로 캐스팅하기 전에 instanceof 연산자를 사용하여 안전하게 캐스팅될 수 있는지 확인합니다.

테스트 코드 작성

  • 단위 테스트: 각 클래스와 메서드의 기능을 검증하는 단위 테스트를 작성하여 코드의 정확성을 보장합니다.
  • 통합 테스트: 여러 클래스가 함께 작동하는지 확인하는 통합 테스트를 작성하여 시스템의 안정성을 검증합니다.

SOLID 원칙 준수

  • SRP(Single Responsibility Principle): 각 클래스는 단 하나의 책임만을 가져야 합니다.
  • OCP(Open-Closed Principle): 기존 코드를 변경하지 않고도 새로운 기능을 추가하거나 수정할 수 있어야 합니다.
  • LSP(Liskov Substitution Principle): 상위 타입 객체를 하위 타입 객체로 안전하게 대체할 수 있어야 합니다.
  • ISP(Interface Segregation Principle): 하나의 인터페이스는 너무 많은 기능을 가지고서는 안 되며, 관련된 기능들을 그룹화하여 여러 인터페이스로 분할해야 합니다.
  • DIP(Dependency Inversion Principle): 추상적인 개념에 의존하고 구체적인 구현에 의존하지 않도록 설계해야 합니다.

oop definition solid-principles

oop definition solid principles

제어 역전(Inversion of Control)이란 무엇일까요?

전통적인 프로그래밍 방식에서는 프로그램 코드가 직접 라이브러리나 프레임워크의 기능을 호출하여 사용합니다. 이 방식은 코드의 의존성이 높아지고 유지 관리가 어려워지는 단점이 있습니다.제어 역전에서는 프로그램 코드가 직접 기능을 호출하는 대신


프로그래밍에서 "상속보다는 구성을 선호하는가?" : 언어 비관점적 관점에서 객체 지향 프로그래밍(OOP) 및 상속 개념 분석

"상속보다는 구성을 선호하는가?"는 객체 지향 프로그래밍(OOP)에서 중요한 질문입니다. 이 질문은 클래스 간의 관계를 설계할 때 상속과 구성 중 어떤 방식을 우선적으로 선택해야 하는지를 고민하는 문제입니다. 두 가지 방식 모두 장단점이 있으며 상황에 따라 적절한 선택이 달라질 수 있습니다


C++에서 클래스와 구조체 사용 시점

1. 기본 접근 지정자:구조체: 기본적으로 모든 멤버가 public으로 접근 가능합니다. 즉, 외부 코드에서 쉽게 변경될 수 있습니다.클래스: 기본적으로 모든 멤버가 private으로 접근 제한됩니다. 외부 코드에서 직접 액세스를 제한하고 데이터 은닉을 통해 코드 보안을 강화합니다