Python

[Python] SOLID_리스코프 치환 원칙(LSP)

UnoCoding 2023. 2. 8. 15:18

SOLID  이란?

클린 코딩 디자인중 하나인 SOLID 원칙에 대해서 천천히 알아보자.

 

  • S: 단일 책임 원칙  
  • O: 개방/폐쇄의 원칙 
  • L: 리스코프 치환 원칙 👈 현위치
  • I: 인터페이스 분리 법칙
  • D: 의존성 역전 원칙

 

리스코프 치환 원칙(LSP)

 

리스코프 치환 원칙(Liskov substitution principle)은 설계 시 안정성을 유지하기 위해 객체 타입이 유지해야하는 일련의 특징을 말한다.

 

하위 클래스는 상위 클래스에서 정의한계약을 따르도록 디자인 해야 한다.

 

간단하게 이야기 하자면 2가지 특징을 가져야 한다

1. 매개변수의 타입이 부모/자식 클래스가 같아야 한다.
    overriding을 한다고 해서 매개변수 타입이 달라지면 일관성이 떨어진다.

2. 반환되는 타입이 같아야 한다.

 

도구를 사용해 LSP 문제 검사.

 

* Mypy를 통한 LSP 문제를 검사.

class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @staticmethod
    def meets_condition(event_data: dict):
        return False


class LoginEvent(Event):
    """로그인 이벤트"""

    @staticmethod
    def meets_condition(event_data: list):
        return bool(event_data)

Error: Argument 1 for "meets_contition" incompatible with supertype "Event"

 

파생 클래스가 부모 클래스에서 정의한 파라미터와 다른 타입을 사용했기 때문에 다르게 동작한다.

 

이렇게 되면 게층 구조의 다형성이 손상된다.

 

* pylint를 통한 LSP 문제 검사

class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @staticmethod
    def meets_condition(event_data: dict):
        return False


class LogoutEvent(Event):
    def __init__(self, raw_data):
        super().__init__(raw_data)

    @staticmethod
    def meets_condition(event_data: dict, override: bool):
        if override:
            return True

        return False

Number of parameters was 1 in '~~' and is now 2 in overridden '~' method 

 

이와 같이 mypy, pylint를 통해서 LSP를 진단해 보았다. 

 

하지만 다음과 같이 자동화된 도구로 검사하기 애매한 경우도 있다.

 

* 도구로 검증이 애매한 경우 

이번 예제는 파라미터가 사전 타입인지, 그리고 "before"와 "after" 키를 가지고 있는지 확인합니다.

"before"와 "after" 키의 값은 또다시 객체를 내포해야 합니다. 이렇게 하면 클라이언트는 KeyError를 받지 않으므로 보다 발전된 캡슐화를 할 수 있다.

class Event:
    def __init__(self, raw_data):
        self.raw_data = raw_data

    @staticmethod
    def meets_condition(event_data: dict):
        return False

    @staticmethod
    def meets_condition_pre(event_data: dict):
        """event_data 유효성 검사"""

        assert isinstance(event_data, dict), f"{event_data} is not dict"
        for moment in ("before", "after"):
            assert moment in event_data, f"{moment} is not in {event_data}"
            assert isinstance(event_data[moment], dict)
class SystemMonitor:
    """시스템에서 발생한 이벤트 분류"""

    def __init__(self, event_data):
        self.event_data = event_data

    def identify_event(self):
        """이벤트 식별"""

        Event.meets_condition_pre(self.event_data)
        event_cls = next(
            (
                event_cls
                for event_cls in Event.__subclasses__()
                if event_cls.meets_condition(self.event_data)
            ),
            UnknownEvent,
        )

        return event_cls(self.event_data)

"before"와 "after"가 필수이고 그 값또한 사전 타입이어야 한다는 조건이 있다.

 

그렇다면 "before"와 "after"에 "sesstion"이라는 Keyword가 포함되지 않았을 경우 어떻게 설계해야 할까?

class TransactionEvent(Event):
    """거래 이벤트"""

    @staticmethod
    def meets_condition(event_data: dict) -> bool:
        return event_data["after"].get("transaction") is not None

위 TransactionEvent가 새로이 추가되었다고 가정해 보자.

class LoginEvent(Event):
    """로그인 이벤트"""

    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 0 and event_data["after"]["session"] == 1
        )


class LogoutEvent(Event):
    """로그아웃 이벤트"""

    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"]["session"] == 1 and event_data["after"]["session"] == 0
        )

위 코드와 같이 "LoginEvent", "LogoutEvent"가 성립되었다고 했을때 "before"와 "after"에 "sesstion" 정보가 없으면 에러가 표시 된다.

 

이 문제는 TransactionEvent와 마찬가지로 대괄호 대신 .get() 매소드를 수정하여 해결할 수 있다.

class LoginEvent(Event):
    """로그인 이벤트"""

    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"].get("session") == 0
            and event_data["after"].get("session") == 1
        )


class LogoutEvent(Event):
    """로그아웃 이벤트"""

    @staticmethod
    def meets_condition(event_data: dict):
        return (
            event_data["before"].get("session") == 1
            and event_data["after"].get("session") == 0
        )

 

LSP는 객체지향 소프트웨어 설게의 핵심이 되는 다형성을 강조하기 때문에 좋은 디자인의 기초가 된다. 인터페이스의 매서드가 올바른 계층구조를 갖도록 하여 상속된 클래스가 부모 클래스와 다형성을 유지하도록 하는 것이다.