파이썬(PYTHON) 클린코드 #7_ SOLID, 개방 폐쇄 원칙(OCP)
안녕하세요. 문범우입니다.
이번 포스팅에서는 SOLID 원칙 중, 개방 폐쇄 원칙(Open/Close Principle)에 대해서 다뤄보도록 하겠습니다.
1. OCP(Open/Close Principle)
개방 폐쇄 원칙(Open/Close Principle)이란 소스가 기능 확장에는 열려있지만, 기능 수정에는 닫혀있어야 한다는 원칙이다. 보다 쉽게 말해서, 새로운 기능을 추가함에 있어서는 신규 기능에 대한 소스 추가만 진행해야 하고 기존의 코드를 수정해야 하는 일은 없어야 한다는 것이다.
만약, 새로운 기능을 추가하는데에 있어서 기존의 코드를 수정해야 한다면 기존의 코드가 좋지 않게 디자인 되었다는 것으로 생각할 수 있다.
바로 예제를 통해 확인해보도록 하자. 다음의 코드는 OCP가 잘 지켜지지 못한 코드이다.
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 | # OCP를 지키지 못한 예제 class Message: """Message 추상 클래스""" def __init__(self, data): self.data = data class FirstGradeMessage(Message): """FirstGrade에 대한 메세지 처리 클래스""" class SecondGradeMessage(Message): """SecondGrade에 대한 메세지 처리 클래스""" class ThirdGradeMessage(Message): """ThirdGrade에 대한 메세지 처리 클래스""" class DefaultGradeMessage(Message): """DefaultGrade에 대한 메세지 처리 클래스""" class GradeMessageClassification(): """Grade에 따른 메세지 분류 클래스""" def __init__(self, data): self.data = data def classification(self): if(self.data['grade'] == 1): return FirstGrade(self.data) elif(self.data['grade'] == 2): return SecondGrade(self.data) elif(self.data['grade'] == 3): return ThirdGrade(self.data) else: return DefaultGrade(self.data) | cs |
code: https://github.com/doorBW/python_clean_code
위의 코드는 특정 grade 별로 메세지를 다르게 처리하기 위해, 수신받은 data의 grade가 어떠한지 분류하는 기능을 담당하고 있다.
먼저 각각의 gradeMessage가 상속하고 있는 Message 클래스를 정의하고, 이후 4개의 grade별 Message클래스를 정의하였다.
마지막, GradeMessageClassification클래스는 수신받은 데이터의 grade에 따라서 올바른 class를 리턴해주고 있다.
위의 코드 또한 현 상황에서 아무런 무리없이 작동할 것이다.
하지만 새로운 요구사항에 따라서 FourthGrade가 추가되었다고 생각해보자.
우리는 FourthGradMessage를 새롭게 정의할 것이다. 이는 신규 기능에 따른 확장이므로 문제가 없다.
하지만 해당 grade를 적절히 분류하기 위해 우리는 기존에 정의되어있던 GradeMessageClassification클래스의 classification 함수에 elif를 추가하여 fourthGrade에 대한 분류를 추가해주어야 한다.
즉 기존 로직에 대한 수정이 발생하게 되는 것이다.
이러한 상황처럼 위의 코드는 신규 기능에 대해서 소스 수정이 닫혀있지 않고 열려있게 되어 OCP에 만족하지 못한다고 볼 수 있다.
별개로, grade가 더욱 다양해진다면 위의 classification함수와 같이 if문에 의한 처리는 코드의 번잡성만 증가시킬 뿐이다.
그럼 이를 어떻게 해결할 수 있을까?
동일한 기능이지만, OCP를 만족하는 아래의 코드를 살펴보자.
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 39 40 41 42 43 44 45 | # OCP가 적용된 예제 class Message: """Message 추상 클래스""" def __init__(self, data): self.data = data @staticmethod def is_collect_grade_message(data: dict): return False class FirstGradeMessage(Message): """FirstGrade에 대한 메세지 처리 클래스""" @staticmethod def is_collect_grade_message(data: dict): return data['grade'] == 1 class SecondGradeMessage(Message): """SecondGrade에 대한 메세지 처리 클래스""" @staticmethod def is_collect_grade_message(data: dict): return data['grade'] == 2 class ThirdGradeMessage(Message): """ThirdGrade에 대한 메세지 처리 클래스""" @staticmethod def is_collect_grade_message(data: dict): return data['grade'] == 3 class DefaultGradeMessage(Message): """DefaultGrade에 대한 메세지 처리 클래스""" class GradeMessageClassification(): """Grade에 따른 메세지 분류 클래스""" def __init__(self, data): self.data = data def classification(self): for grade_message_cls in Message.__subclasses__(): try: if grade_message_cls.is_collect_grade_message(self.data): return grade_message_cls(self.data) except KeyError: continue return DefaultGradeMessage(self.data) | cs |
code: https://github.com/doorBW/python_clean_code
우선 가장 크게 변화된 점은, Message 클래스에서 is_collect_grade_message 함수가 추가되었고, 모든gradeMessage클래스에서 이를 재정의하고 있다.
이후 classification을 살펴보면, 이전의 if문과 달리, Message.__subclasses__()를 통해, Message 클래스를 상속받는 모든 클래스를 가져와, is_collect_grade_message 함수를 호출하여 수신받은 data의 grade를 확인한다.
이와 같이 코드를 작성하면 새롭게 FourthGrade가 추가되더라도, 아래와 같이 FourthGradeMessage 클래스만 정의해주면 될 것이다.
1 2 3 4 5 6 | # OCP가 적용된 예제를 기반으로 grade 추가시에는 아래와 같이 Message 클래스를 상속받는 클래스만 생성해주면 된다. class FourthGradeMessage(Message): """FourthGrade에 대한 메세지 처리 클래스""" @staticmethod def is_collect_grade_message(data: dict): return data['grade'] == 4 | cs |
code: https://github.com/doorBW/python_clean_code
이처럼 FourthGrade라는 새로운 요구사항이 추가되어도 우리는 기존의 로직에 손대거나 수정할 필요가 없어진다.
이렇게 OCP를 지키게 되면 새로운 요구사항에 대해서 기존의 로직을 수정해야 하는일이 없게 되므로 시스템 안정성 뿐만 아니라 유지보수 측면에서도 좋은 이점을 가져올 수 있다.