without haste but without rest

SOLID Principle 본문

Computer Science

SOLID Principle

JinungKim 2022. 2. 10. 10:51
파이썬 클린코드

SRP - Single Reponsibility Principle

소프트웨어 컴포넌트가 단 하나의 책임을 져야한다는 원칙이다. 즉 클래스는 하나의 구체적인 일만을 담당한다. 따라서 도메인의 문제가 변경되면 클래스를 변경하게 되며 그 이외의 이유로 클래스를 수정해야한다면 추상화가 잘못 되어서 클래스에 너무 많은 책임이 있다는 것을 뜻한다. 

 

SRP를 지키지 않은 예

class SystemMoniter:
    def load_activity(self):
    """ 소스에서 처리할 이벤트를 가져오기"""
    
    def identify_events(self):
    """ 가져온 데이터를 파싱해서 도메인"""
    
    def stream_events(self):
    """파싱한 이벤트를 외부 에이전트로 전송"""

이 클래스의 문제점은 독립적인 동작을 하는 메서드를 하나의 인터페이스에 모두 정의했다. 각각의 메서드는 나머지 부분과 독립적으로 수행할 수 있다.

SystemMonitor 클래스는 이벤트를 가져오고, 파싱하고, 외부로 전송하는 모든 역할을 담당한다. layered arichitecture에 대응 시켜보면 presentation layer, business layer, persistent layer 모두를 클래스 하나에서 처리를 하는 것이다.

 

책임 분산

솔루션을 보다 관리하기 쉽게 하기 위해 모든 메서드를 다른 클래스로 분리하여 각 클래스마다 단일 책임을 갖게 한다.

(인터페이스를 분리한다.)

class AlertSystem:
    def run(self):
    """ 시스템 실행 """
    
    
class ActivityReader:
    def load_activity(self):
    """ 소스에서 처리할 이벤트를 가져오기"""
    

class SystemMoniter:
    def identify_events(self):
    """ 가져온 데이터를 파싱해서 도메인"""


class OutPut:
    def stream_events(self):
    """파싱한 이벤트를 외부 에이전트로 전송"""

각자의 책임을 가진 여러 객채로 분리하고, 이들 객체들과 협력하여 동일한 기능을 수행하는 객체를 만들 수 있다. 이때 각각의 객체들은 특정한 기능을 캡슐화하여 나머지 객체들에 영향을 미치지 않으며 명확하고 구체적인 의미를 갖는다. 

데이터 소스에서 이벤트를 로드하는 방법을 변경해도 AlertSystem은 변경사항과 관련이 없으므로 SystemMonitor와 OutPut은 아무것도 수정하지 지 않아도 된다. 

새로운 클래스는 유지보수뿐 아니라 재사용이 쉬운 인터페이스를 정의한다. 애플리케이션의 다른 부분에서 로그를 읽는다고 가정하면, 이 디자인을 적용하면 단순히 ActivityReader 타입의 객체를 사용하면 된다.

처음 디자인의 경우 상속 시 필요하지 않은 메서드까지 함께 받게 되므로 비효율적이지만 위 구조의 경우 필요한 메서드만 상속 받는다. 단, 각 클래스가 하나의 메서드를 가져야 한다는 의미는 아니다. 처리해야 할 로직이 같은 경우 하나의 클래스에 여러 메서드를 추가할 수 있다.

 


Open/Close Principle

모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙이다. 클래스를 디자인할 때는 유지보수가 쉽도록 로직을 캡슐화하여 확장에는 개방되고 수정에는 폐쇄되어야 한다. 간단히 말해서 확장가능하고, 새로운 요구사항이나 도메인 변화에 잘 적응하는 코드를 작성해야 한다는 뜻이다. 새로운 문제가 발생할 경우 새로운 것을 추가만 할 뿐 기존 코드는 그대로 유지해야 한다는 뜻이다. 

새로운 기능을 추가하다가 기존 코드를 수정했다면 그것은 기존 로직이 잘못 디자인되었다는 것을 뜻한다. 이상적으로는 요구사항이 변경되면 새로운 기능을 구현하기 위한 모듈만 확장하고 기존 코드는 수정 하면 안 된다.

이 원칙은 여러가지 소프트웨어의 추상화에 적용된다. 클래스뿐 아니라 모듈에도 적용할 수 있다.

 

개방/폐쇄 원칙을 지킨 디자인의 예시

"""Clean Code in Python - Chapter 4
The open/closed principle.
Example with the corrected code, in order to comply with the principle.
Extend the logic to prove the ``SystemMonitor`` class is actually closed with
respect to the types of events.
"""


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

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


class UnknownEvent(Event):
    """A type of event that cannot be identified from its data"""


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
        )


class TransactionEvent(Event):
    """Represents a transaction that has just occurred on the system."""

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


class SystemMonitor:
    """Identify events that occurred in the system
    >>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
    >>> l1.identify_event().__class__.__name__
    'LoginEvent'
    >>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}})
    >>> l2.identify_event().__class__.__name__
    'LogoutEvent'
    >>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}})
    >>> l3.identify_event().__class__.__name__
    'UnknownEvent'
    >>> l4 = SystemMonitor({"after": {"transaction": "Tx001"}})
    >>> l4.identify_event().__class__.__name__
    'TransactionEvent'
    """

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

    def identify_event(self):
        for event_cls in Event.__subclasses__():
            try:
                if event_cls.meets_condition(self.event_data):
                    return event_cls(self.event_data)
            except KeyError:
                continue
        return UnknownEvent(self.event_data)

각각의 서브 클래스는 Event 클래스를 상속받고, meets_condition 메서드를 구현한다. 

SystemMonitor 클래스는 Event클래스의 서브 클래스를 확인하면서 이벤트를 식별한다. 


LSP - Liskov substituion Principle

설계 시 안정성을 유지하기 위해 객체 타입이 유지해야하는 일련의 특성을 의미한다. LSP의 주된 생각은 어떤 클래스에서든 클라이언트는 특별한 주의를 기울이지 않고도 하위 타입을 사용할 수 있어야한다는 것이다. 어떤 하위 타입을 사용해도 실행에 따른 결과를 염려하지 않아야 한다. 즉 클라이언트는 완전히 분리되어 있으며 클래스 변경 사항과 독립되어야 한다.

Liskov 01 원칙에 의하면 만약 S가 T의 하위 타입이라면 프로그램을 변경하지 않고 T타입의 객체를 S타입의 객체로 치환이 가능해야 한다.

이 원칙의 배경은 계층 구조가 올바르게 구현되었다면 클라이언트 클래스가 주의를 기울이지 않고도 모든 하위 클래스의 인스턴스로 작업할 수 있어야한다는 것이다. 이러한 개체는 서로 바꿔서 사용할 수 있다.

LSP 위반 기준은 명확하다. 파생 클래스가 부모 클래스에서 정의한 파라미터와 다른 타입을 사용할 수 없다. 호출자(클라이언트)는 아무런 차이를 느끼지도 않고 투명하게 서브 클래스를 사용할 수 있어야 한다. 이 두가지 타입의 객체를 치환해도 애플리케이션 실행에 실패해서는 안 된다. 그렇지 않다면 계층 구조의 다형성이 손상된 것이다. 

 

부모 클래스는 클라이언트와 계약을 정의한다. 서브 클래스는 그 계약을 따라야 한다. 예를 들면 다음과 같다. 

- 하위 클래스는 부모 클래스에 정의된 것 보다 사전조건을 엄격하게 만들면 안 된다. 

- 하위 클래스는 부모 클래스에 정의된 것 보다 약한 사후조건을 만들면 안 된다.

 

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):
        """Precondition of the contract of this interface.
        Validate that the ``event_data`` parameter is properly formed.
        """
        assert isinstance(event_data, dict), f"{event_data!r} is not a dict"
        for moment in ("before", "after"):
            assert moment in event_data, f"{moment} not in {event_data}"
            assert isinstance(event_data[moment], dict)


class UnknownEvent(Event):
    """A type of event that cannot be identified from its data"""


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
        )


class TransactionEvent(Event):
    """Represents a transaction that has just occurred on the system."""

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


class SystemMonitor:
    """Identify events that occurred in the system
    >>> l1 = SystemMonitor({"before": {"session": 0}, "after": {"session": 1}})
    >>> l1.identify_event().__class__.__name__
    'LoginEvent'
    >>> l2 = SystemMonitor({"before": {"session": 1}, "after": {"session": 0}})
    >>> l2.identify_event().__class__.__name__
    'LogoutEvent'
    >>> l3 = SystemMonitor({"before": {"session": 1}, "after": {"session": 1}})
    >>> l3.identify_event().__class__.__name__
    'UnknownEvent'
    >>> l4 = SystemMonitor({"before": {}, "after": {"transaction": "Tx001"}})
    >>> l4.identify_event().__class__.__name__
    'TransactionEvent'
    """

    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)

ISP - Interface Segregation Principle

객체 지향적인 용어로 인터페이스는 객체가 노출하는 메서드의 집합이다. 즉 객체가 수신하거나 해석할 수 있는 모든 메시지가 인터페이스를 구성하며, 이것들은 다른 클라이언트에서 호출할 수 있는 요청들이다. 

파이썬에서 인터페이스는 클래스 매서드의 형태를 보고 암시적으로 정의된다. 이것은 파이썬이 덕 타이핑(duck typing) 원리를 따르기 때문이다. 오랫동안 덕 타이핑은 파이썬에서 인터페이스를 정의하는 유일한 방법이었다. 파이썬3(PEP-3119)에서 인터페이스를 다른 방식으로 정의하는 추상 기본 클래스 개념을 도입했다. 추상 기본 클래스는 파생 클래스가 구현해야 할 일부분을 기본 동작 또는 인터페이스로 정의하는 것이다. 


DIP - Dependency Inversion Principle

의존성을 역전시킨다는 것은 코드가 세부 사항이나 구체적인 구현에 적응하도록 하지 않고, API 같은 것에 적응하도록 하는 것이다. 이는 코드가 깨지거나 손상되는 취약점으로부터 보호해주는 흥미로운 디자인 원칙을 제시한다.

추상화를 통해 세부 사항에 의존하지 않도록 해야 하지만, 반대로 세부 사항 (구체적인 구현)은 추상화에 의존해야 한다. A와 B의 두 객체가 상호 교류한다고 가정한다. A는 B의 인스턴스를 사용하지만 우리가 B모듈을 직접 관리하지 않는다. 외부 라이브러리 또는 다른 팀의 모듈을 등을 사용하는 경우이다. 만약 코드가 B에 크게 의존하면 B코드가 변경되면 원래의 코드가 쉽게 깨지게 된다. 이를 방지하기 위해서 의존성을 거꾸로 뒤집어서 역전시켜야 한다. 즉 B가 A에 적응해야 한다. 이렇게 하려면 인터페이스를 개발하고 코드가 B의 구체적인 구현에 의존하지 않게 해야한다. 대신에 정의한 인터페이스에 의존적이도록 해야 한다. 


 

'Computer Science' 카테고리의 다른 글

Layered Architecture  (0) 2022.02.10
애플리케이션 배포 프로세스  (0) 2022.01.13
Comments