ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SOLID] 리스코프 원칙 법칙이란 (Liskov Substitution Principle, LSP)
    공부 2021. 8. 16. 18:43

    리스코프 치환 법칙은 SOLID 원칙에서 L에 해당하는 법칙입니다. 해당 법칙은 상위 타입의 객체를 하위 타입의 객체로 치환해도 동작에 문제가 없어야 합니다. 즉, B가 A의 자식일 때, A 타입을 사용하는 부분에서 B로 치환해도 문제없이 동작이 되어야 합니다.

    아래에서 정사각형과 직사각형 예를 들어 설명합니다.

    정사각형은 직사각형이 될 수 있지만 직사각형은 정사각형이 될 수 없습니다. 즉, 정사각형은 직사각형의 자식이라고 판단한 다음, 아래와 같이 코드를 작성합니다.

    from dataclasses import dataclass, field
    
    @dataclass()
    class Rectangle:
        _width: int = field(init=False)
        _height: int = field(init=False)
    
        @property
        def width(self) -> int:
            return self._width
    
        @property
        def height(self) -> int:
            return self._height
    
        @width.setter
        def width(self, width: int):
            self._width = width
    
        @height.setter
        def height(self, height: int):
            self._height = height
    
        @property
        def area(self) -> int:
            return self._height * self._width
    
    class Square(Rectangle):
        @Rectangle.width.setter
        def width(self, number: int):
            self._width = number
            self._height = number
    
        @Rectangle.height.setter
        def height(self, number: int):
            self._width = number
            self._height = number

    직사각형 클래스인 Rectangle 클래스를 정의한 다음 이를 상속하여 정사각형 클래스 Square 클래스를 만든 예시입니다. 정사각형의 경우, 넓이와 높이가 모두 같으므로 setter를 재정의 하였습니다.

    이 코드를 사용한다면 리드코프 치환 원칙을 위배하게 됩니다.

    def increase_height(__rect: Rectangle):
        __rect.height += 30
    
    rect: Rectangle = Rectangle()
    rect.width = 10
    rect.height = 20
    
    increase_height(rect)
    print(rect.area == 500)  # True
    
    rect2: Rectangle = Square()
    rect2.width = 10
    rect2.height = 20
    
    increase_height(rect2)
    print(rect2.area == 500)  # False

    부모 타입인 Rectangle에서 자식 타입인 Square로 변경하면 width와 height를 계산하는 식이 달라지므로 부모와 자식 클래스 간의 동작이 서로 다릅니다.

    직사각형은 정사각형이지만 정사각형은 직사각형이다. 고로 직사각형 → 정사각형이라는 논리적인 의미로 상속을 구현할 순 있지만 위 코드 예시를 통해 리스코프 치환 법칙에 위배되므로 상속 관계를 유지할 수 없습니다.

    만약 위 코드에서 Square 클래스일 때도 성립하게 만들기 위해 아래처럼 바꾼다면 이번엔 개방 폐쇄 원칙 (OCP)를 위배하게 됩니다.

    def increase_height(__rect: Rectangle):
        if isinstance(__rect, Square):
            __rect = Rectangle()
            __rect.width = 10
            __rect.height = 20
    
        __rect.height += 30
    
        return __rect
    
    rect2: Rectangle = Square()
    rect2.width = 10
    rect2.height = 20
    rect2 = increase_height(rect2)
    print(rect2.area == 500)  # True

    위의 increase_width 함수를 확장한 것이 아닌 이미 개발되어 있는 코드를 수정했기 때문에 위배를 하게 됩니다.

    이러한 코드의 올바른 수정 방법은 아래와 같이 직사각형 → 정사각형 관계가 아닌 별개의 도형으로 취급을 해야 합니다.

    from abc import ABCMeta, abstractmethod
    from dataclasses import dataclass
    
    class Shape(metaclass=ABCMeta):
        @property
        @abstractmethod
        def area(self):
            pass
    
    @dataclass()
    class Rectangle(Shape):
        _width: int
        _height: int
    
        @property
        def width(self) -> int:
            return self._width
    
        @property
        def height(self) -> int:
            return self._height
    
        @width.setter
        def width(self, width: int):
            self._width = width
    
        @height.setter
        def height(self, height: int):
            self._height = height
    
        @property
        def area(self) -> int:
            return self._height * self._width
    
    @dataclass()
    class Square(Shape):
        _width: int
    
        @property
        def width(self) -> int:
            return self._width
    
        @width.setter
        def width(self, number: int):
            self._width = number
    
        @property
        def area(self) -> int:
            return self._width ** 2
    
    def increase_height(__rect: Rectangle):
        __rect.height += 30
    
    def increase_width(__rect: Square):
        __rect.width += 30
    
    rect: Rectangle = Rectangle(10, 20)
    increase_height(rect)
    print(rect.area == 500)  # True
    
    square = Square(10)
    increase_width(square)
    print(square.area == 1600)  # True

    도형이라는 의미인 Shape 추상 클래스를 만든 다음, 직사각형과 정사각형 각각 해당 추상 클래스를 상속받아 구현한 케이스로 직사각형 → 정사각형 상속 관계를 없애버려 리스코프 치환 법칙 위배 문제를 해결했습니다.

    요약

    상속 관계를 구현할 때, 해당 법칙을 잘 상기하자

    리스코프 치환 법칙을 보장하겠다고 개방 폐쇄 원칙을 위배하지 말자.

    댓글