티스토리 뷰

Books

파이썬 클린 코드 (4/10)

최성훈 2022. 5. 7. 02:59
반응형

4장. SOLID 원칙

👉 S: 단일 책임 원칙
👉 O: 개방/폐쇄의 원칙
👉 L: 리스코프(Liskov) 치환 원칙
👉 I: 인터페이스 분리 원칙
👉 D: 의존성 역전 원칙

 


단일 책임 원칙 (S)

단일 책임 원칙(Single Responsibility Principle - SRP)은 소프트웨어 컴포넌트 (일반적으로 클래스)가 단 하나의 책임을 져야한다는 원칙이다. 클래스가 유일한 책임이 있다는 것은 하나의 구체적인 일을 담당한다는 것을 의미하며, 따라서 변화해야 할 이유는 단 하나뿐이다.

도메인의 문제가 변경되면 클래스를 업데이트해야 한다. 다른 이유로 클래스를 수정해야 한다면 추상화가 잘못되어 클래스에 너무 많은 책임이 있다는 것을 뜻한다.

 

여기서 말하는 도메인의 문제가 뭘까?

 

그리고 다른 이유로 클래스를 수정해야 할 잘못된 예가 뭐가 있을까?

 

아직은 추상적인 감밖에 잡히지 않네.. 🧐

 

너무 많은 책임을 가진 클래스

class SystemMonitor:
    def load_activity(self):
        """소스에서 처리할 이벤트를 가져오기"""
    
    def identify_events(self):
    	"""가져온 데이터를 파싱하여 도메인 객체 이벤트로 변환"""
    
    def stream_events(self):
        """파싱한 이벤트를 외부 에이전트로 전송"""
이 클래스의 문제점은 독립적인 동작을 하는 메서드를 하나의 인터페이스에 정의했다는 것이다. 각각의 동작은 나머지 부분과 독립적으로 수행할 수 있다.

이 디자인 결함은 유지보수를 어렵게 하여 클래스가 경직되고 융통성 없으며 오류가 발생하기 쉽게 만든다. 이 예제에서 각 메서드는 클래스의 책임을 대표한다. 각각의 책임마다 수정 사유가 발생한다. 즉 메서드마다 다양한 변경의 필요성이 생기게 된다.

 

즉, 쉽게 말해서 SytemMonitor라는 클래스는 이벤트를 (1) 로드하고 (2) 파싱하며 (3) 외부로 전송한다.

 

책에서는 SystemMonitor 클래스의 책임이 너무 크다고 보는 것인데, 내 생각에는 "이벤트를 로드/파싱/전송하는 역할이 그렇게 큰 책임인가?" 싶긴 하다. 🧐

 

책임 분산

class AlertSystem:
    def run(self):
        """메소드 실행"""

class ActivityReader(AlertSystem):
    def load(self):
        """소스에서 처리할 이벤트를 가져오기"""

class SystemMonitor(AlertSystem):
    def identify_event(self):
        """가져온 데이터를 파싱하여 도메인 객체 이벤트로 변환"""

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

 


개방/폐쇄 원칙 (O)

개방/폐쇄 원칙(Open/Close Principle)은 모듈이 개방되어 있으면서도 폐쇄되어야 한다는 원칙이다(물론 다른 측면에서의 개방과 폐쇄이다).

클래스를 디자인할 때는 유지보수가 쉽도록 로직을 캡슐화하여 확장에는 개방되고 수정에는 폐쇄되도록 해야 한다.

간단히 말해서 확장 가능하고, 새로운 요구사항이나 도메인 변화에 잘 적응하는 코드를 작성해야 한다는 뜻이다. 즉 새로운 문제가 발생할 경우 새로운 것을 추가만 할 뿐 기존 코드는 그대로 유지해야 한다는 뜻이다.

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

 

OCP 최종 정리

... 이 원칙은 다형성의 효과적인 사용과 밀접하게 관련되어 있다. 다형성을 따르는 형태의 계약을 만들고 모델을 쉽게 확장할 수 있는 일반적인 구조로 디자인하는 것이다.

... (중략)...

마지막 중요한 요점은 코드를 변경하지 않고 기능을 확장하기 위해서는 보호하려는 추상화에 대해서 적절한 폐쇄를 해야 한다는 것이다.

 

음.. 이 부분을 읽다 보니 내가 짠 코드가 생각이 난다.

 

일종의 보일러 플레이팅 된 함수인데, 새로운 요구사항이나 도메인 변화에 따라서 해당 함수를 고치려면 그 함수를 사용한 모든 코드를 수정해야 한다.

 

프로젝트 내에서 워낙 많은 곳에서 쓰이다 보니 이제는 함부로 수정하기가 쉽지 않은데, 이 부분을 읽다 보니 내가 안티 패턴 코드를 작성한 것 같다. 😢

 


리스코프 치환 법칙 (LSP)

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

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

 

메서드 서명의 잘못된 데이터타입 검사

class Event:
    ...
    def meets_condition(self, event_data: dict) -> bool:
        return False

class LoginEvent(Event):
    def meets_condition(self, event_data: list) -> bool:
        return bool(event_data)

 

LoginEvent 클래스의 meet_condition() 메소드는 부모 클래스의 인자와 다르다. (dict <-> list)

 

리스코프 치환 법칙에 따르면 Event 객체와 LoginEvent 객체의 호출자는 아무 변경 없이 두 개의 객체를 사용할 수 있어야 한다.

 

하지만 두 클래스의 meets_condition() 인자가 서로 달라서 두 클래스 간의 다형성이 손상되고, 호출자는 서로 다른 방법으로 두 객체를 사용해야만 한다.

 

이는 메소드 인자뿐만 아니라 반환값에서도 판단할 수 있는데, boolean 값이 아닌 다른 타입을 반환해도 문제가 발생한다. 리턴 타입을 boolean에서 다른 값으로 변경한다면, 계약(contract)을 위반하게 되고 이는 프로그램이 정상적으로 동작한다는 기대를 할 수 없게 된다.

 

LSP 최종 정리

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

 

LSP를 지키지 않는다는 것은 부모 클래스의 계약과 호환되지 않는 확장을 한다는 것이므로 클라이언트와 계약이 깨지게 된다.

 

이 상태에서 억지로 확장하려고 한다면 결과적으로 수정에 대해 폐쇄되어야 한다는 개방/폐쇄 원칙(OCP)을 깨게 된다.

 

이는 리스코프 치환 법칙(LSP)개방/폐쇄 원칙(OCP)에 기여한다고 말할 수 있다.

 


인터페이스 분리 법칙

인터페이스 분리 원칙(Interface Segregation Principle, ISP)은 이미 반복적으로 재검토했던 "작은 인터페이스"에 대한 가이드라인을 제공한다.

파이썬에서 인터페이스는 클래스 메서드의 형태를 보고 암시적으로 정의된다. 이것은 파이썬이 소위 말하는 덕 타이핑(duck typing) 원리를 따르기 때문이다.

... 즉 클래스의 유형, 이름 , docstring, 클래스 속성 또는 인스턴스 속성에 관계없이 객체의 본질을 정의하는 것은 궁극적으로 메서드의 형태이다. ..(중략).."어떤 새가 오리처럼 걷고 오리처럼 꽥꽥 소리를 낸다면 오리여야만 한다."는 데서 덕 타이핑이라고 불린다.

 

파이썬에 인터페이스가 없는 이유는 동적 바인딩 때문이다.

 

Java와 같은 컴파일 언어의 경우 클래스가 어떤 메소드를 가지고 있는지 미리 선언해야 하며, 이는 컴파일 타임에 체크된다.

 

반면에 파이썬의 경우엔 동적 바인딩 때문에 클래스와 관련된 속성들이 런타임에 체크된다.

 

이런 특징 때문에 파이썬은 덕 타이핑을 따를 수밖에 없다.

 

책에서는 파이썬의 인터페이스가 암시적으로 정의되는 이유는 덕 타이핑을 따르기 때문인 것처럼 말하는데, 그게 아니라 파이썬이 동적 언어이기 때문에 덕 타이핑을 따를 수밖에 없고, 결과적으로 클래스의 메소드 형태를 보고 판단할 수밖에 없게 되는 것이다.

 

추상적으로 말하자면 ISP는 다음을 뜻한다. 다중 메서드를 가진 인터페이스가 있다면 매우 정확하고 구체적인 구분에 따라 더 적은 수의 메서드(가급적이면 단 하나)를 가진 여러 개의 메서드클래스로 분할하는 것이 좋다는 것이다.

 

책에서는 여러 개의 메서드로 분할하는 것이 좋다고 말하는데.. 이는 번역 오류인 것 같다.

 

메서드가 아니라 클래스가 아닐까 싶다.

 

 

SRP와 유사하지만 주요 차이점은 ISP는 인터페이스에 대해 이야기하고 있다는 점이다. 따라서 이것은 행동의 추상화이다.

 

인터페이스 분리 법칙은 이 한 문장으로 요약할 수 있을 것 같다.

 

 

의존성 역전

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

추상화를 통해 세부 사항에 의존하지 않도록 해야 하지만, 반대로 세부 사항 (구체적인 구현)은 추상화에 의존해야 한다.

 

만약 A 객체가 B 객체를 사용 중이라면, A 객체는 B 객체에 의존적이게 된다.

 

B 객체에 변경사항이 생긴다면 A 객체도 변경이 필요할 수 있다는 것이다.

 

이를 방지하기 위해 B 객체를 사용하기 위한 인터페이스(파이썬에서는 추상 클래스)를 사용해야 한다.

 

즉, A-B 클래스의 상호작용에서, A가 B에 의존적인 상황을 예방하기 위해 A-C-B와 같이 인터페이스를 두는 것이다.

 

이렇게 하면 A가 B에 의존하는 것이 아닌, B가 C 인터페이스를 따르게 한다고 볼 수 있다는 것이다.

반응형

'Books' 카테고리의 다른 글

파이썬 클린 코드 (5/10)  (0) 2023.01.16
파이썬 클린 코드 (3/10)  (0) 2022.03.18
파이썬 클린 코드 (2/10)  (3) 2022.03.04
파이썬 클린 코드 (1/10)  (0) 2022.03.04
댓글