TigerCow.Door


안녕하세요. 문범우입니다.

이번 포스팅에서는 SOLID 원칙에 대해서 알아봅니다. SOLID는 5가지 원칙을 줄여서 말하는 내용인데, 한번에 5가지 모두를 알아보지 않고, 각 원칙에 대해서 코드로 함께 살펴보며 총 5개의 포스팅으로 나누어 설명드릴 예정입니다.



0. SOLID란?


우선 솔리드, SOLID는 객체 지향 프로그래밍 및 설계의 다섯가지 기본 원칙을 이야기한다. 프로그램이 시간이 지나도 유지 보수 및 확장이 쉬울 수 있도록 하기 위한 원칙이며, 우리가 학습하고자 했던 클린코드를 위한 원칙이기도 하다. 


조금 더 구체적으로, SOLID는 아래와 같은 5가지 원칙을 이야기 한다.


1. Single Responsibility Principle(단일 책임 원칙)


2. Open/Closed Principle(개방/폐쇄 원칙)


3. Liskov Substitution Principle(리스코프 치환 원칙)


4. Interface Segregation Principle(인터페이스 분리 원칙)


5. Dependency Inversion Principle(의존관계 역전 원칙)


이제 우리는 각 원칙에서 강조하는 것과, 실제로 그것을 코드로서 간단하게 살펴보며 이해를 해보도록 하겠다.




1. SRP(Single Responsibility Principle)


단일 책임 원칙(SRP)이란 하나의 클래스는 하나의 책임만 가지며, 그 책임에 대해 완전히 캡슐화 해야 함을 말한다. 

클래스가 하나의 책임만 가진다는 것은, 하나의 클래스는 하나의 일을 담당하고 있으며, 클래스를 변경해야 할 이유는 오직 한가지가 존재한다는 것으로 생각할 수 있다. 이러한 원칙은, 코드의 응집력을 높이는데 도움을 준다. 


간혹 하나의 클래스가 다양한 책임을 지고 있는 경우가 있는데, 이러한 클래스(객체)를 일컬어 신(god) 객체라고 이야기 한다. 이러한 신 객체가 존재하면 시스템에 대한 유지보수가 어려워진다.


어떤 면에서 단일 책임 원칙은, 관계형 데이터베이스 설계에서의 정규화 개념과 유사하다고 생각할 수 있다. 만약 객체의 속성이나 메서드 중에서 서로 특성이 다른 요소(그룹)가 발견되면 그러한 것들을 적절히 분리시켜야 한다.


이제 실제로 코드를 통해 너무 많은 책임을 가진 클래스와, 이를 단일 책임 원칙에 따라 분리한 예제를 살펴보도록 하자.


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
class Course:
    """너무 많은 책임을 가진 Course 클래스"""
    
    def __init__(self, code, name, schedule, pf):
        self.code = code
        self.name = name
        self.schedule = schedule
        self.pf = pf
    
    def connect(self, con)->bool:
        """DB connecting"""
        pass
    
    def close(self)->bool:
        """DB close"""
        pass
    
    def get_course_by_pf(self, professor)->list:
        """professor로 강의 찾기 함수"""
        pass
    
    def save_course(self, Course)->bool:
        """DB에 course 저장하기"""
        pass
    
    def update_course(self, code)->bool:
        """특정 code를 가진 course를 DB update 하기"""
        pass
cs

code: https://github.com/doorBW/python_clean_code


먼저 위의 Course 클래스를 살펴보자.

해당 Course 클래스는 너무 많은 책임을 가지고 있다. 클래스의 생성자 __init__함수가 정의되어 있으면서 DB접근과 관련된 함수들도 함께 정의되어 있다.

즉, Course 객체의 속성들에 대한 관리를 하고 있으면서, Course의 DB 내용에 대한 관리도 하고 있다. 이러한 경우 DB에 대한 시스템 변경이 있을 때나, Course의 속성에 대한 변경이 있을 때 동일한 클래스를 수정하고 서로 영향이 없도록 신경써야 한다.

따라서 위와 같은 클래스는 아래와 같이 분리할 수 있다.


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
class Course:
    """SRP원칙을 준수한 Course 클래스"""
    
    def __init__(self, code, name, schedule, pf):
        self.code = code
        self.name = name
        self.schedule = schedule
        self.pf = pf
 
class CourseDB:
    """SRP원칙을 준수한 CourseDB 클래스"""
    def __init__(self, con):
        self.con = con
    
    def connect(self)->bool:
        """DB connecting"""
        pass
    
    def close(self)->bool:
        """DB closing"""
        pass
    
    def get_course_by_pf(self, professor)->list:
        """professor로 강의 찾기 함수"""
        pass
    
    def save_course(self, Course)->bool:
        """DB에 course 저장하기"""
        pass
    
    def update_course(self, code)->bool:
        """특정 code를 가진 course를 DB update 하기"""
        pass
cs

code: https://github.com/doorBW/python_clean_code


이전과 달리 Course 클래스와 CourseDB 클래스로 나누어, DB를 관리하는 클래스를 따로 분리하였다. 

이렇게 됨으로써 각 클래스가 자신이 담당한 하나의 일에 대해서만 책임을 지게 된다.


이번에는 다른 예제를 살펴보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Human:
    """추상화 과정이 필요한 Human 클래스"""
    def __init__(self, name, sex):
        self.name = name
        self.sex = sex
        
    def go_restroom(self):
        """화장실 가는 함수"""
        if(self.sex == '남자'):
            print("남자화장실로 간다.")
        elif(self.sex == '여자'):
            print("여자화장실로 간다.")
        else:
            print("성별을 지정해주세요.")
 
cs

code: https://github.com/doorBW/python_clean_code


위의 Human 클래스에서는 객체 생성시에 성별을 정의하게 되어있다. 그리고 go_restroom에서는 성별에 따라 분기처리하여 로직을 처리한다. 즉, Human클래스는 성별이 다른 두가지 케이스에 대한 책임을 동시에 가지고 있다. 이러한 경우 Human 클래스를 다음과 같이 추상화시킴으로써 해결할 수 있다.


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
from abc import *
 
class HumanBase(metaclass=ABCMeta):
    def __init__(self, name):
        self.name = name
    
    @abstractmethod
    def go_restroom(self):
        pass
    
class Male(HumanBase):
    def __init__(self, name):
        super().__init__(name)
        self.sex = "남자"
    
    def go_restroom(self):
        print("남자화장실로 간다.")
        
class Female(HumanBase):
    def __init__(self, name):
        super().__init__(name)
        self.sex = "여자"
        
    def go_restroom(self):
        print("여자화장실로 간다.")
cs

code: https://github.com/doorBW/python_clean_code


위와 같이 추상클래스, HumanBase 클래스를 만들어 주었다. 기본적으로 생성시에는 이름만 받고 이를 상속받은 클래스들은 go_restroom을 구현하게끔 하였다.

그리고 HumanBase를 상속받는 Male 클래스와 Female 클래스를 정의함으로써, 성별이 다른 경우에 대해 각 클래스가 유일한 책임을 가지도록 하였다.


이렇게 우리가 SOLID에서 단일 책임 원칙에 대해 알아보면서 대표적인 두가지 예제를 살펴보았다.

첫번째, Course 클래스에서는 단일 클래스에서 너무 많은 액션, 업무를 담당하고 있어 이를 분리함으로써 단일 클래스가 하나의 책임만 가지도록 하였다.

두번째, Human 클래스에서는 보다 높은 추상과 과정을 통해서 각 클래스가 하나의 책임만 가지도록 설정하였다.


이렇게 단일 책임 원칙을 지킨 경우에 클래스에 대한 외부 영향도를 최소화 할 수 있으며, 결국 유지보수나 확장 면에서 보다 효율적일 수 있다.


블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요


안녕하세요. 문범우입니다.

이번 포스팅에서는 파이썬에서의 인자(Argument)에 대해서 알아보도록 하겠습니다.

파이썬에서 인자가 어떻게 작동하는지, 그리고 가변인자와 같은 개념들도 함께 알아보도록 하겠습니다.



0. 인자(Argument)와 매개변수(Parameter)


본격적으로 인자에 대해서 알아보기전에, 자주 헷갈릴 수 있는 인자와 매개변수에 대해서 잠깐 짚고 넘어가도록 하자. 물론 이는 파이썬언어 뿐만이 아니라 다른 언어에서도 혼동되서 사용될 수 있는 개념이다.


1
2
3
4
5
def func1(param1, param2):
    print(f"param1:{param1}, param2:{param2}")
 
func1("AA","B")
# param1:AA, param2:B
cs


위의 코드를 살펴보며 이야기해보자.

함수 func1에는 param1과 param2가 전달되도록 정의되어있다. 이렇게 함수가 정의되는 내용에 포함되는 특성을 매개변수(Parameter)라고 한다. 즉, 함수 func1는 2개의 매개변수, param1과 param2를 전달받아서 이를 출력하는 기능을 하는 것이다.

그리고 4번 라인에서는 func1에 "AA"라는 값과 "B"라는 값을 전달하며 함수를 호출하고 있다. 이때 함수에 전달하는 값을 인자(Argument)라고 한다.


이러한 차이에 의해서 매개변수(Parameter)는 변수(Variable)로 보아야 하며, 인자(Argument)는 값(Value)로 보아야한다. 두 개념이 비슷하다고 생각할 수 있으나, 함수에 대해서 이야기를 할때에는 구분해서 사용해야 혼동되지 않을 수 있다.




1. 파이썬에서 인자의 전달방식


앞에서 인자에 대해서 설명할 때, 함수를 호출하며 값으로 전달되는 것이라고 설명했다. 하지만 사실 함수에 '값(Value)'로 전달되는 것은 특정 상황에서만이다.

함수에 인자가 전달될 때에는, 실제로 그 값이 넘어가는 "값에 의한 호출(Call by Value)"와 값이 참조하고 있는 참조값이 넘어가는 "참조에 의한 호출(Call by Reference)" 두가지가 존재한다.

(만약 당신이 C언어를 했다면 포인터를 공부하면서 이 개념을 접했을 수도 있다.)


각각의 호출방법이 어떤 차이가 있는지 먼저 코드로 살펴보자.


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
def call_by_value(param1:int)->None:
    param1 += 1
    print(f"call by value: {param1}")
    
def call_by_ref(param2:list)->None:
    param2[0+= 1
    print(f"call by ref: {param2}")
    
val1 = 3
val2 = [3]
 
print(f"before val1:{val1}")
call_by_value(val1)
print(f"after val1:{val1}")
print("- - - - - - - - - - ")
print(f"before val2:{val2}")
call_by_ref(val2)
print(f"after val2:{val2}")
# before val1:3
# call by value: 4
# after val1:3
# - - - - - - - - - - 
# before val2:[3]
# call by ref: [4]
# after val2:[4]
cs


위의 코드에서는 call_by_value 함수와 call_by_ref함수를 정의하여 사용하고 있다. 각각의 함수가 받는 매개변수의 타입은 다르지만, 그 이유에 대해서는 뒤에서 이야기하겠다. 우선 "값에 의한 호출"과 "참조에 의한 호출"에 대한 차이를 이해해보자.


우리가 주목해야 할 것은 19번줄부터 25번줄까지의 결과이다. val1과 val2 모두 함수를 거치기 이전에 출력을 한 다음, 함수 호출시 함수 내부에서 출력한 후에, 함수 호출이 끝난 후에 출력을 한다.

먼저 val1의 값을 보면, 함수 호출 이전에 3이란 값을 출력하고, 함수 내부에서 출력될 때에는 1을 더한 4가 출력되고 있다. 그리고 함수가 끝난 다음에는 값이 변화하지 않고 그대로 3을 출력한다. 즉, 함수에서 더한 1이 val1에 영향을 미치지 않았다.

그럼 이번에는 val2의 값을 살펴보자. 당연히 함수 호출 이전에는 3을 출력했다. 그리고 함수 내부에서 출력될 때에는 1을 더한 4가 출력되었다. 여기까지는 val1과 같았지만, 함수 호출이 끝난 뒤 val2를 출력했을 때에는, val1과 달리 기존의 val2=3값에서 변화가 되어 4를 출력하였음을 볼 수 있다.


즉, val1은 함수에서 내부적으로 1을 더했지만, 기존의 값에 영향을 미치지 않았고, val2는 함수에서 더한 1이 실제 값이 영향을 미쳤다.

이것이 바로 "값에 의한 호출"과 "참조에 의한 호출"에 대한 차이로 발생한 것이다.


값에 의한 호출을 할 때에는, 함수로 인자가 전달될 때 동일한 "값"을 가진 객체를 복사하여 함수에 전달한다. 즉, 위의 코드에서 val1이 call_by_value함수에 전달될 때에는 사실 val1자체가 전달된 것이 아니고, val1이 가진 값을 동일하게 가진 또다른 객체가 함수로 전달된 것이다. 그리고 함수에서 1을 더한것은 val1이 아니고 val1과 똑같은 값을 가진 "val1과 다른 객체"에 더한 것이다. 이로 인해 함수에서 전달받은 값에 대해 변화를 시도해도 기존의 val1값은 변화가 없는 것이다.


이와 달리, 참조에 의한 호출을 할 때에는, 함수로 인자가 전달될 때 실제로 인자가 가진 "참조 값"을 전달한다. 참조 값을 전달한다는 것은 실제로 인자 객체를 그대로 전달한다고 생각해도 된다. 즉, 값에 의한 호출에서와 달리 인자를 복사한 객체를 전달하는 것이 아니라, 말 그대로 인자 그 자체를 전달하는 것이다. 따라서 함수내부에서 전달받은 인자에 대해 변화를 주면, 실제로 그 인자에 영향이 있게 된다.


이것이 "값에 의한 호출"과 "참조에 의한 호출"의 차이이다.


C언어의 경우에는 인자를 값에 의한 호출로 전달할 것인지, 참조에 의한 호출로 전달할 것인지를 명시적으로 정해줄 수 있다. 하지만 파이썬 같은 경우는 이를 명시적으로 나타내지 않는다.

그럼 파이썬에서는 "값에 의한 호출"과 "참조에 의한 호출"을 어떻게 구분할 수 있을까?

위의 코드를 통해 짐작했을 수도 있다. 파이썬에서는 따로 호출에 대한 명시적인 구분을 두지 않고, 함수에 전달되는 인자의 타입(type)에 의해서 결정된다.

변수 타입에는 불변형(immutable) 객체와, 가변형(mutable) 객체가 있다. 즉 값의 수정이 허용되지 않는 변수 타입이 있으며, 값의 수정이 허용되는 변수 타입이 있다.

이를 통해, 함수에 전달되는 인자가 불변형 객체, 값의 수정이 허용되지 않는 변수타입이라면 이는 "값에 의한 호출"로 함수에 전달된다. 하지만 함수에 전달되는 인자가 가변형 객체, 값의 수정이 허용되는 변수타입이라면 이는 "참조에 의한 호출"로 함수에 전달된다.

때문에 이를 파이썬 공식 문서에서는 call by value나 call by reference라는 설명이 아닌, call by assignment라고 설명하고 있다. 함수에 할당되는 변수의 타입에 따라서 그 방식이 달라지기 때문이다.

파이썬에서 불변형객체, 가변형객체는 아래와 같이 나뉜다.


* 불변형(immutable) 객체

int, float, str, tuples 등


* 가변형(mutable) 객체

list, set, dict 등




2. 가변 인자


파이썬에서는 다른 언어와 같이, 가변인자 함수를 지원한다. 가변인자 함수라 함은 인자의 개수가 정해지지 않은 함수라고 생각하면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def args_test_func1(a, b, c):
    print("* call args_test_func1")
    print(f"a:{a}, b:{b}, c:{c}, a+b+c:{a+b+c}")
    print("* END args_test_func1", end='\n\n')
    
def args_test_func2(a, b, *args):
    print("* call args_test_func2")
    print(f"a:{a}, b:{b}, args:{args}, a+b+c:{a+b+sum(args)}")
    print("* END args_test_func2", end='\n\n')
    
args_test_func1(1,2,3)
args_test_func2(1,2,3)
args_test_func2(1,2,3,4,5,6)
# * call args_test_func1
# a:1, b:2, c:3, a+b+c:6
# * END args_test_func1
 
# * call args_test_func2
# a:1, b:2, args:(3,), a+b+c:6
# * END args_test_func2
 
# * call args_test_func2
# a:1, b:2, args:(3, 4, 5, 6), a+b+c:21
# * END args_test_func2
cs


위의 코드에서 2개의 함수를 정의했다. args_test_func1 함수는 우리가 기존에 보던 함수처럼 a,b,c 3개의 매개변수를 정의한 함수이다. 아래 결과에서도 3개의 인자를 전달해 함수가 올바르게 기능했다.

이와 달리 args_test_func2 함수에서는 a,b라는 매개변수와 함께, *args 라는 매개변수를 정의하였다. 이렇게 *를 이용한 매개변수는 가변인자를 받을 수 있는 것으로 해석된다. 즉, 개수가 정해지지 않은 인자를 args라는 이름으로 받아서 처리하겠다는 것이다.

실제로 13번 줄에서 3개 이상의 인자를 함수에 전달했는데, 3부터 6까지는 args라는 이름으로 받아서 처리한 것을 볼 수 있다. 약간의 차이점은 args로 받은 인자는 튜플로 받아서 처리했다는 점이다.


이렇게 *를 사용하는 것을 패킹(packing)한다고 말하기도 하는데, 다음의 코드를 보면 그 의미가 더 와닿을 것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def packing_func1(a, b, c):
    print(f"a:{a}, b:{b}, c:{c}, a+b+c:{a+b+c}")
    
packing_list = [1,2,3]
packing_func1(packing_list)
# ---------------------------------------------------------------------------
# TypeError                                 Traceback (most recent call last)
# <ipython-input-15-aaa44dbee827> in <module>
#       3 
#       4 packing_list = [1,2,3]
# ----> 5 packing_func1(packing_list)
 
# TypeError: packing_func1() missing 2 required positional arguments: 'b' and 'c'
 
packing_func1(*packing_list)
# a:1, b:2, c:3, a+b+c:6
 
cs


packing_func1은 이전와 같이 3개의 매개변수를 정의한 함수이다. 그리고 이 함수를 호출하기 위해 3개의 int값을 가지는 리스트, packing_list를 정의했다. 그리고 해당 리스트를 그대로 인자로 전달하면 에러가 발생한다. 타입의 맞고 틀림을 떠나서, packing_func1에서는 3개의 인자가 필요한데, 1개의 인자만 전달했기 때문이다.

이를 해결하기 위해서는 파이썬의 *를 이용한 패킹 기법을 사용하면 된다. 즉, 15번 줄과 같이 packing_list를 *를 통해 패킹함으로써 3개의 인자에 대응하는 값을 전달할 수 있다.




3. 가변 키워드 인자


가변 키워드 인자는 위에서 알아본 가변 인자와 비슷하다. 가변인자에서는 별표( * )를 하나 사용했지만, 가변 키워드 인자에서는 별표( * )를 2개 사용한다.


1
2
3
4
5
def kwargs_test_func1(**kwagrs):
    print(f"kwagrs:{kwagrs}")
    
kwargs_test_func1(key="value", test="wow")
# kwagrs:{'key': 'value', 'test': 'wow'}
cs


가변 키워드 인자가 가변 인자 개념과 다른 점은, **kwagrs로 정의된 매개변수는 인자로 받은 값을 "딕셔너리"형태로 패킹한다는 것이다.

따라서 **가 붙은 매개변수는 딕셔너리형태로 함수 내에서 활용할 수 있다.

또한 가변 인자에서와 같이, 기존의 딕셔너리 자료형을 함수에 전달할 때, 다음과 같이 사용할 수 있다.


1
2
3
4
5
6
7
8
9
def kwargs_test_func2(a, b, c):
    print(f"a:{a}, b:{b}, c:{c}, a+b+c:{a+b+c}")
    
kwagrs_dict = {
    'a'1'b'2'c'3
}
 
kwargs_test_func2(**kwagrs_dict)
# a:1, b:2, c:3, a+b+c:6
cs



블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요



안녕하세요. 문범우입니다.

이번 포스팅에서는 상속과 컴포지션에 대해서 알아보도록 하겠습니다.

어떤 경우에 상속을 쓰는 것이 올바른 것인지, 그리고 파이썬에서 상속과 파이썬을 어떻게 사용하는지에 대해서 코드로 함께 살펴보겠습니다.



1. 상속(Inheritance)


상속이란 기본적으로 자신이 가진 속성과 메소드를 물려주는 클래스(부모클래스, Parent class, Super class)와 이를 물려받는 클래스(자식클래스, Child class, Sub class)로 이루어진다.

객체 지향적 소프트웨어를 디자인하고 구현할 때 상속은 중요한 개념으로, 또 자주 사용된다. 하지만 이를 다시 생각해보자. 상속개념을 활용하면 부모클래스와 자식클래스간에 강력한 결합력(Coupling)이 발생하게 된다.

좋은 코드, 유지보수를 위한 코드를 생각할 때 결합력을 낮추고, 응집도는 높여야 한다고 했는데, 이러한 점을 고려하면 상속이 무조건적으로 좋은 방안은 아닐 수 있다.

즉, 상속이란 것이 좋은 경우와 그렇지 않은 경우를 잘 선별해가며 활용할 수 있어야한다.


만약 새로운 자식클래스를 만들었을 때 해당 자식클래스가 올바르게 정의된 클래스인지 확인하기 위해서는 상속받은 모든 메서드를 실제로 자식클래스에서 사용하는지를 살펴보아야 한다. 만약 대부분의 메소드를 실제로 필요로 하지 않는다면 다음과 같은 이유로 설계상의 실수일 수 있다.


- 부모클래스가 잘 정의된 인터페이스 대신 막연한 정의와 너무 많은 책임을 가진 경우

- 자식클래스가 확장하려고 하는 부모클래스의 적절한 세분화가 아닌경우


따라서, 상속을 쓰는 올바른 경우는 새로 정의하고자 하는 자식클래스가 부모클래스의 기능을 그대로 물려받으면서 이중 특정 기능을 수정하고자 하거나, 추가적인 기능을 구현하고자 할 때이다.


또한 상속에서도, 인터페이스 정의는 또 다른 상속의 올바른 경우로 생각할 수 있다. 어떤 객체에 인터페이스 방식을 강제하고자 할 때, 실제 기능을 구현하지 않은 추상클래스를 만들고, 이를 상속받는 자식클래스에서 실제로 기능을 구현하게 한다. 


그럼 올바르지 않은 상속의 경우는 어떠한 케이스가 있을까? 바로 예시 코드를 살펴보자.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import collections
from datetime import datetime
 
class TransactionalPolicy(collections.UserDict):
    """잘못된 상속의 예시"""
    
    def change_in_policy(self, customer_id, **new_policy_data):
        self[customer_id].update(**new_policy_data)
 
 
policy = TransactionalPolicy({
    "client001":{
        "fee"1000.0,
        "expiration_date": datetime(2020,1,3),
    }
})
 
policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 3, 0, 0)}
 
policy.change_in_policy("client001",expiration_date=datetime(2020,1,4))
policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 4, 0, 0)}
 
cs


위의 코드를 살펴보면 TransactionalPolicy 클래스는 파이썬 내장 collections의 사전자료형을 상속받아 구현하였다. 그리고 TransactionalPolicy 클래스에서 사용하고자 하는 change_in_policy 함수를 구현하였다.


이후 policy 라는 TransactionalPolicy 객체를 만들어 추가한 함수를 사용하였다. 이처럼 원하는 기능을 수행하는 클래스를 구현하였지만, 과연 이 클래스가 사전자료형을 상속하는 것이 올바른 것일까? 파이썬에서 객체가 사용가능한 메서드를 출력해주는 dir함수를 이용하여 policy객체가 이용가능한 메서드를 살펴보자.


1
2
3
4
5
6
7
8
dir(policy)
# ['_MutableMapping__marker', '__abstractmethods__', '__class__', '__contains__', '__copy__', '__delattr__',
# '__delitem__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__',
# '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__module__',
# '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__setattr__', '__setitem__',
# '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_abc_impl',
# 'change_in_policy', 'clear', 'copy', 'data', 'fromkeys', 'get', 'items',
# 'keys', 'pop', 'popitem', 'setdefault', 'update', 'values']
cs


policy 객체를 보면 우리가 추가로 구현한 change_in_policy 라는 함수 이외에도 다양한 함수가 있다. 정말 해당 객체에서 pop, popitem 등의 함수가 필요로 한 것일까? 필요하지 않을텐데 포함되어 있다. 더군다나 해당 함수들은 public 메서드이기 때문에 이를 사용하게 될 가능성도 있다. 이러한 것을 살펴보아, TransactionalPolicy 클래스가 사전자료형을 상속받은 것은 올바르지 않다고 볼 수 있다.


더군다나, 의미적으로 보았을 때 부모클래스로부터 상속을 받은 자식클래스는 개념적으로 확장되거나, 세부적이라는 것을 의미해야 한다. 하지만 TransactionalPolicy 클래스가 사전자료형에 대해서 개념적으로 확장되거나 세부적이라는 것이라고 생각할 수 있을까?


그럼, 우리는 TransactionalPolicy 클래스를 정의하기 위해서 어떻게 해야할까?


이러한 경우 컴포지션이라는 개념을 활용해볼 수 있다.



2. 컴포지션(Composition)


컴포지션이란, 상속과 다르게 단순히 사용한다는 개념이다. 즉, 기존의 상속 개념에서의 자식클래스가 부모클래스의 모든 속성을 물려받는게 아니라, 자식클래스가 필요한 속성만 부모클래스로부터 가져와 사용하는 것이다.

일반적으로 상속은 암시적 선언이라고 하며, 컴포지션은 명시적 선언이라고 한다.


그럼 바로 컴포지션은 어떻게 사용되는지 위에서의 예시를 가져와 살펴보자.


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
class NewTransactionalPolicy:
    """컴포지션을 활용한 예시"""
    
    def __init__(self, policy_data, **extra_data):
        self._data = {**policy_data, **extra_data}
    
    def change_in_policy(self, customer_id, **new_policy_data):
        self._data[customer_id].update(**new_policy_data)
        
    def __getitem__(self, customer_id):
        return self._data[customer_id]
    
    def __len__(self):
        return len(self._data)
 
new_policy = NewTransactionalPolicy({
    "client001":{
        "fee"1000.0,
        "expiration_date": datetime(2020,1,3),
    }
})
 
new_policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 3, 0, 0)}
 
new_policy.change_in_policy("client001",expiration_date=datetime(2020,1,4))
new_policy["client001"]
# {'fee': 1000.0, 'expiration_date': datetime.datetime(2020, 1, 4, 0, 0)}
cs


이전의 예시와 달리 NewTransactionalPolicy 클래스에서는 사전자료형을 상속받지 않았다.

하지만 __init__함수에서 _data를 private로 선언하면서 사전자료형의 데이터로 초기화한다. 그리고 이후 우리가 사용하고자 하는 change_in_policy함수와 __getitem__함수에서 _data, 즉 사전 자료형을 활용하여 구현하고자 하는 기능을 구현하였다.


이렇게 직접 사전자료형을 상속받지 않고, 단순히 사전자료형을 가져와서 사용하였다.

이를 통해 추후 사전자료형에서 변경이 발생하더라도 그 인터페이스만 잘 유지된다면 NewTransactionalPolicy에서는 별도의 변경이 필요없다. 더군다나 이전에 dir함수를 통해 policy 객체가 사용가능한 메서드를 살펴본 것 처럼 new_policy 객체를 살펴보면 다음과 같다.


1
2
3
4
5
6
dir(new_policy)
# ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',
#  '__getattribute__', '__getitem__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
#  '__len__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
#  '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
#  '_data', 'change_in_policy']
cs


확실히 상속을 받았던 policy 객체에 비해, 사용 가능하지만 필요하지 않았던 함수가 사라진 것을 확인할 수 있다.



이렇게 상속과 컴포지션은 단순히 별개의 것이 아니라, 기존의 클래스를 재사용하는 상황에 있어서, 상속을 사용해야 할지 컴포지션을 사용해야 할 지 신중히 판단해야 한다. 단순히 현상황만을 고려한다기 보다는 상속과 컴포지션의 특성과, 장단점을 잘 생각하고 활용해야 새롭게 정의하는 클래스가 추후 가져올 문제를 최소화 할 수 있다.



블로그 이미지

Tigercow.Door

Back-end / Python / Database / AI / Algorithm / DeepLearning / etc

댓글을 달아 주세요