Python 은 강력한 객체지향을 지원하는 언어 중에 하나이다. 그리고 객체지향에서 반복의 제거와 코드의 재사용성을 높이기 위해 상속이라는 개념을 자주 활용한다.
상속은 강력한 도구 중에 하나이지만 그만큼 큰 잠재적 위험도 가지고 있다. 이러한 위험은 주로 상속받은 클래스가 부모 클래스와 지나치게 높은 결합력을 가지는 데 있어서 발현된다.
" 경험상 좋은 코드는 대체로 높은 응집력과 낮은 결합력을 가지고 있었다. "
이러한 리스크를 피하면서 상속을 잘 활용하기 위해서는 먼저 자식 클래스가 상속받은 부모 클래스의 메서드를 적극 활용하는지 확인해 볼 필요가 있다. 만약 대부분의 메서드가 필요하지 않거나 다시 재정의 (overriding)를 해야 한다면 아래와 같은 설계상의 오류를 가지고 있다고 볼 수 있다.
1. 상위 클래스가 적절한 Interface로 정의되지 못하고 지나치게 장황하거나 많은 책임을 지고 있다.
단일책임원칙, 객체를 설계할 때 주로 활용되는 개념이다. 이를 위반하고 객체가 지나치게 많은 책임을 지게 된다면 코드의 분리가 매끄럽게 이루어지지 못하고 문맥의 흐름을 저해 할 수가 있다. 그리고 이는 해당 객체의 사용성을 떨어트리고 이러한 객체를 상속받은 하위 객체들은 전체 SW 입장에서 큰 리스크로 다가올 수 있다.
2. 하위 클래스는 확장하려고 하는 상위 클래스의 적절한 세분화가 아니다.
쉽게 말해서 굳이 상속을 안 받아도 구현 가능한 클래스를 부모 클래스를 상속받아 구현하려고 하면서 생기는 오류이다. 이러한 설계는 상위클래스에 대해 필요 없는 결합력만 높여 객체의 사용성을 떨어트리는 결과를 가져온다.
꼭 python이 아니더라도 대부분의 객체지향 언어에서 통용되는 개념들이다.
비단 도메인과 객체의 설계 뿐만 아니라 단순히 코드 재사용을 위해 상속을 남발할 때도 여러 문제를 야기시킬 수 있다. 그리고 이러한 패턴의 상속은 생각보다 자주 발생한다.
아래 예제를 통해 확인해 보자
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import collections from datetime import datetime # Bad example class BadTransactionPolicy(collections.UserDict): def change_in_policy(self, customer_id, **policy_data): self[customer_id].update(**policy_data) # Class 이름만 보고 어떻게 사전형 데이터 인지 알 수 있을까? # Dict 에서 사용하는 수많은 하위 메서드들이 포함되어 있다. # -> 이로 인해 Dict 클래스와 지나치게 높은 결합력을 가지게 된다. ## 이것이 구현 객체(Dict)를 도메인 객체(Policy)와 혼합할 때 발생하는 문제 ## -> 수정된 Dict 객체가 필요 할 떄만 Dict 를 확장해야 한다. # Good example (Using Composition) class GoodTransactionPolicy: def __init__(self, policy_data, **extra_data): self._data = {**policy_data, **extra_data} def change_in_policy(self, customer_id, **policy_data): self._data[customer_id].update(**policy_data) # 사전의 Proxy 역할 def __getitem__(self, customer_id): return self._data[customer_id] def __len__(self): return len(self._data) my_policy = GoodTransactionPolicy({ "client01": { "fee": 1000, "exp_date": datetime(2023, 6, 1) } }) print(my_policy["client01"]) | cs |
예제를 보면 도메인 객체에 대한 첨자형 접근(dictionary)을 위해 Dict를 상속 받았는데 이는 필요 없이 Dict 클래스와의 결합력만 높이는 결과를 가져온다. 그리고 사용하지도 않는 Dict 메서들을 상속받아서 시간 / 공간 복잡도 측면에서도 좋지 않은 trade off를 가져오게 된다.