Computer Science/Python

Beyond 상속

0cool 2023. 2. 12. 10:05

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(202361)
    }
})
 
print(my_policy["client01"])
cs


예제를 보면 도메인 객체에 대한 첨자형 접근(dictionary)을 위해 Dict를 상속 받았는데 이는 필요 없이 Dict 클래스와의 결합력만 높이는 결과를 가져온다. 그리고 사용하지도 않는 Dict 메서들을 상속받아서 시간 / 공간 복잡도 측면에서도 좋지 않은 trade off를 가져오게 된다.