-
[디자인 패턴] SOLID 의 Open Closed Principle알고리즘/디자인 패턴(Design Patterns) 2022. 11. 30. 17:26반응형
디자인 패턴에 대한 정리 내용 중 SOLID 원칙의 두 번째 원칙인 Open Closed Principle 즉, 개방 폐쇄 원칙에 대하여 적어보겠습니다. SOLID 설계 원칙은 유지 보수가 쉽고 재사용 가능하도록 하고, 확장 가능한 개발을 위해 지켜지고 있습니다. 제 두 번째 디자인 패턴 글에서는 C ++의 계방 폐쇄 원칙과 이를 사용하였을 때, 장점 등을 보겠습니다.
이 글 시리즈에서는 총 SOLID의 5가지 원칙에 대해 살펴보겠습니다.
- SRP - Single Responsibility Principle
- OCP - Open/Closed Principle
- LSP - Liskov Substitution Principle
- ISP - Interface Segregation Principle
- DIP - Dependency Inversion Principle
계방 폐쇄 원칙 정리
클래스는 확장에는 열려있어야 하고, 수정에는 닫혀있어야 한다.
저에게는 SOLID의 5가지 원칙 중, 가장 와닿으면서 가장 지키기 어려운 원칙인 거 같다고 제 개인적 생각을 남깁니다 ㅎㅎ 계방 폐쇄 원칙은 클래스의 기능을 내부 기능의 수정 없이, 확장시킬 수 있다는 원칙을 내포하고 있습니다. 언뜻 봐서는 어떻게 이런 일이 가능할지 짐작이 안 가지만, C++ 및 기타 프로그래밍 언어에서는 OOP의 기능을 활용하여, 다형성 및 템플릿을 통해 구현하고 있습니다.
계방 폐쇄 원칙의 위반 예시
- 코드를 보여드리기 전에, 우선적으로 상황 설명을 들겠습니다. 데이터 베이스에 Color, 그리고 Size 별로 각각 다른 상품이 정의되어 있다고 할 때,
- Color 그리고 Size를 조합하여, 상품을 구별해내는 Filter를 만들고 싶다고 하겠습니다. 그러면 Filter를 구현한 코드를 살펴보겠습니다.
#include <fstream> #include <iostream> #include <string> #include <vector> using namespace std; enum class Color { Red, Green, Blue }; enum class Size { Small, Medium, Large }; struct Product { string name; Color color; Size size; }; struct ProductFilter { using Items = vector<Product *>; public: // Color 별로 구분하는 멤버함수 Items ByColor(Items &items, const Color color) { Items result; for (auto &i : items) { if (i->color == color) result.emplace_back(i); } return result; } // Size 별로 구분하는 멤버 함수 Items BySize(Items &items, const Size size) { Items result; for (auto &i : items) { if (i->size == size) result.emplace_back(i); } return result; } // Size 와 Color 별로 구분하는 멤버 함수 Items BySizeAndColor(Items &items, const Color color, const Size size) { Items result; for (auto &i : items) { if (i->color == color && i->size == size) result.emplace_back(i); } return result; } };
- 위 코드를 대략 설명해보자면, Product를 크기, 그리고 사이즈를 통해, 각각의 특성에 따른 구별 Filter를 만들어 냈습니다. 마찬가지로 위 코드는 실제 구현상 아무 문제가 존재하지 않습니다.
- 하지만, 여기서 기능을 추가해야 하는 일이 생길 때, 어려움이 생기게 됩니다.. 다른 사용자가 또 다른 Filter의 요구사항을 원한다고 가정했을 때 어떻게 대처해야 할지 먼저 생각해봅니다. 안에다 모든 멤버 함수를 정의하게 된다면, 확장에 대한 유연성을 가지지 못하게 됩니다.
- 이때 고려해봐야 할 것이 계방 폐쇄 원칙입니다. 확장에는 열려있지만, 수정에는 닫혀 있도록 강제할 수 있습니다.
어떻게 계방 폐쇄 원칙을 지킬 건데?
많은 해결법이 있지만, 상속을 활용하여, 분류(Specification)를 위한 행동을 추상화시켜주고, 해당 분류를 활용하여, result를 반환해주는 Filter 객체를 따로 정의해 줄 것입니다. 좀 더 간단히 설명하자면,
- Specification 이란 분류에 대한 추상화 클래스를 작성해준 다음,
- Specification을 상속받은, SizeSpecification / ColorSpecification들이 존재합니다. 이때, 이러한 분류 기법을 사용하는 Filter 객체도 필요한데, Filter 또한 Specification과 마찬가지로 추상화 클래스로 만들어 주게 됩니다.
- 그리고 다양한 필터를 만들어주기 위해, Filter를 상속받는 객체들을 사용자가 커스텀하여 사용할 수 있게끔 합니다.
말로 하는 것보다 코드 한 줄로 설명드리는 게 편할 것 같습니다. 우선 분류를 위한 specification 클래스를 작성해보겠습니다.
template <typename T> struct Specification { virtual ~Specification() = default; virtual bool is_satisfied(T* item) const = 0; }; struct ColorSpecification:public Specification<Product> { Color eColor; ColorSpecification(Color color):eColor(color){} bool is_satisfied(Product *item) const override {return item->color == eColor;} }; struct SizeSpecification :public Specification<Product> { Size eSize; SizeSpecification(Size size) : eSize(size) {} bool is_satisfied(Product *item) const override { return item->size == eSize; } };
Specification에 대한 추상 클래스를 작성해주고, 사용자가 Specification 에 대한 명세만을 구현해 준다면, Color / Size 등을 위한 필터와 과 같이 손쉽게 모듈을 확장할 수 있도록 해주었습니다.
그다음으로 Specification으로 동작하는 Filter를 만들어 보겠습니다.
template <typename T> struct Filter { virtual vector<T *> filter(vector<T *> items, const Specification<T> &spec) = 0; }; struct BetterFilter : Filter<Product> { vector<Product *> filter override (vector<Product *> items, const Specification<Product> &spec) { vector<Product *> result; for (auto &p : items) if (spec.is_satisfied(p)) result.push_back(p); return result; } };
Filter 클래스는 Specification 이 어떻게 작동하는지 궁금하지 않습니다. 해당 Specification을 활용해서, 어떻게 Filter를 작동시킬지는 사용자가 filter 멤버 함수를 오버 라이딩하면서, 동작을 정의해줄 수 있습니다.
이렇게 Filter와 Specification 추상화 클래스를 정의해 주었는데요, 이를 통해 명확히 계방 폐쇄 원칙을 실현시킬 수 있었습니다.
- Specification 객체를 상속받아, 내부 동작 명세서만 구현해준다면, 손쉽게 확장성을 가져갈 수 있다.
- Filter 객체를 상속받아, 마음에 안 드는 Filter 기능을 내 입맛대로 수정할 수 있다.
이렇게 추상화 객체를 추가함으로써, 계방 폐쇄 원칙을 지킬 수 있게 되었습니다.
반응형
계방 폐쇄 원칙의 장점
1. 확장성
프로그램의 작은 변경으로 인하여, 종속적인 모듈들의 변경이 연쇄적으로 발생하게 된다면 해당 프로그램은 나쁜 프로그램의 특성을 지니게 됩니다. 그 프로그램은 깨지기 쉬우며, 예측할 수 없고, 재사용할 수 없게 됩니다. 이러한 요구사항에 맞춰 개방 폐쇄 원칙은 이 점을 명확하게 짚고 넘어갈 수 있게끔 해줍니다. 요구 사항이 변경되면 이미 작동하는 이전 코드를 변경하는 것이 아니라 새 코드를 추가하여 이러한 모듈의 동작을 확장합니다.
- 로버트 마틴 (Robert Martin)모듈의 분리와 재사용성을 위해, 개방 폐쇄 원칙(Open Closed Principle)은 프로그램이 확 작성을 가질 수 있도록 할 수 있습니다.
2. 유지 보수 측면의 장점
- 개방 폐쇄 원칙의 또 다른 주요 이점은 인터페이스가 느슨한 결합도를 가능하게 한다는 점입니다. 인터페이스의 구현은 서로 독립적이기 때문에, 서로의 영향에 의한 코드 복잡도를 신경 써주지 않아도 됩니다.
- 따라서 클라이언트의 계속되는 변경 요구 사항에 쉽게 대처할 수 있습니다.
3. 유연성
- 개방 폐쇄 원칙은 플러그인과 미들웨어 설계에도 적용될 수 있다는 장점이 존재합니다.
결론
현실에서 코드를 작성하다 보면, 개방 폐쇄 원칙을 완벽하게 적용하기 결코 쉽지 않다는 사실을 알 수 있습니다.. 모든 클래스가 완벽히 닫혀 있고 또한 열려 있다는 것 또한 불가능할 수 있습니다. 하지만 개방 폐쇄 원칙을 염두에 두고 코드를 설계해나간다면 훨씬 유연한 아키텍처를 얻을 수 있음에는 분명해 보이는 것 같습니다. 개방 폐쇄 원칙을 따르기 위해서, 많은 디자인 패턴들이 고안되어 있으며, 이를 활용하여 좋은 설계를 해내는 것이 중요한 사실인 것 같습니다.
반응형'알고리즘 > 디자인 패턴(Design Patterns)' 카테고리의 다른 글
[디자인 패턴] SOLID 의 Interface Segregation Principle (0) 2022.12.13 [디자인 패턴] SOLID 의 Liskov Substitution Principle (0) 2022.12.02 [디자인 패턴] SOLID 의 Single Responsibility (2) 2022.11.30