Corgi Dog Bark

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [디자인 패턴] SOLID 의 Open Closed Principle
    알고리즘/디자인 패턴(Design Patterns) 2022. 11. 30. 17:26
    반응형

    디자인 패턴에 대한 정리 내용 중 SOLID 원칙의 두 번째 원칙인 Open Closed Principle 즉, 개방 폐쇄 원칙에 대하여 적어보겠습니다. SOLID 설계 원칙은 유지 보수가 쉽고 재사용 가능하도록 하고, 확장 가능한 개발을 위해 지켜지고 있습니다. 제 두 번째 디자인 패턴 글에서는 C ++의 계방 폐쇄 원칙과 이를 사용하였을 때, 장점 등을 보겠습니다.

     

    이 글 시리즈에서는 총 SOLID의 5가지 원칙에 대해 살펴보겠습니다.

    1. SRP - Single Responsibility Principle
    2. OCP - Open/Closed Principle
    3. LSP - Liskov Substitution Principle
    4. ISP - Interface Segregation Principle
    5. 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 객체를 따로 정의해 줄 것입니다. 좀 더 간단히 설명하자면, 

    1.  Specification 이란 분류에 대한 추상화 클래스를 작성해준 다음,
    2.  Specification을 상속받은, SizeSpecification / ColorSpecification들이 존재합니다. 이때, 이러한 분류 기법을 사용하는 Filter 객체도 필요한데, Filter 또한 Specification과 마찬가지로 추상화 클래스로 만들어 주게 됩니다.
    3. 그리고 다양한 필터를 만들어주기 위해, 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 추상화 클래스를 정의해 주었는데요, 이를 통해 명확히 계방 폐쇄 원칙을 실현시킬 수 있었습니다.

    1. Specification 객체를 상속받아, 내부 동작 명세서만 구현해준다면, 손쉽게 확장성을 가져갈 수 있다.
    2. Filter 객체를 상속받아, 마음에 안 드는 Filter 기능을 내 입맛대로 수정할 수 있다.

    이렇게 추상화 객체를 추가함으로써, 계방 폐쇄 원칙을 지킬 수 있게 되었습니다.

     

     

    반응형

     

     

     


     

    계방 폐쇄 원칙의 장점

    1. 확장성

    프로그램의 작은 변경으로 인하여, 종속적인 모듈들의 변경이 연쇄적으로 발생하게 된다면 해당 프로그램은 나쁜 프로그램의 특성을 지니게 됩니다. 그 프로그램은 깨지기 쉬우며, 예측할 수 없고, 재사용할 수 없게 됩니다. 이러한  요구사항에 맞춰 개방 폐쇄 원칙은 이 점을 명확하게 짚고 넘어갈 수 있게끔 해줍니다. 요구 사항이 변경되면 이미 작동하는 이전 코드를 변경하는 것이 아니라 새 코드를 추가하여 이러한 모듈의 동작을 확장합니다.
    - 로버트 마틴 (Robert Martin)

    모듈의 분리와 재사용성을 위해, 개방 폐쇄 원칙(Open Closed Principle)은 프로그램이 확 작성을 가질 수 있도록 할 수 있습니다.

     

    2. 유지 보수 측면의 장점

    • 개방 폐쇄 원칙의 또 다른 주요 이점은 인터페이스가 느슨한 결합도를 가능하게 한다는 점입니다. 인터페이스의 구현은 서로 독립적이기 때문에, 서로의 영향에 의한 코드 복잡도를 신경 써주지 않아도 됩니다.
    • 따라서 클라이언트의 계속되는 변경 요구 사항에 쉽게 대처할 수 있습니다. 

     

    3. 유연성

    • 개방 폐쇄 원칙은 플러그인과 미들웨어 설계에도 적용될 수 있다는 장점이 존재합니다. 

     

     


     

    결론

    현실에서 코드를 작성하다 보면, 개방 폐쇄 원칙을 완벽하게 적용하기 결코 쉽지 않다는 사실을 알 수 있습니다.. 모든 클래스가 완벽히 닫혀 있고 또한 열려 있다는 것 또한 불가능할 수 있습니다. 하지만 개방 폐쇄 원칙을 염두에 두고 코드를 설계해나간다면 훨씬 유연한 아키텍처를 얻을 수 있음에는 분명해 보이는 것 같습니다. 개방 폐쇄 원칙을 따르기 위해서, 많은 디자인 패턴들이 고안되어 있으며, 이를 활용하여 좋은 설계를 해내는 것이 중요한 사실인 것 같습니다. 

    반응형

    댓글

Designed by Tistory.