Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
관리 메뉴

codingfarm

1장. SOLID 디자인 원칙 - 1 본문

소프트웨어 공학/디자인패턴

1장. SOLID 디자인 원칙 - 1

scarecrow1992 2019. 10. 20. 00:13

SOLID는 다음과 같은 디자인 원칙들을 아우르는 약어이다.

단일 책임 원칙(Single Responsibility Principle, SRP)

열림-닫힘 원칙(Open-Closed Principle, OCP)

리스코프 치환 원칙(Liscov Substitution Principle, LSP)

인터페이스 분리 원칙(Interface Segregation Principle, ISP)

의존성 역전 원칙(Dependenc Inversion Principle, DIP)

 

이 5가지 원칙은 디자인 패턴의 존재 이유에 전반적으로 녹아들어가 있다.

 

 

1. 단일 책임 원칙(Single Responsibilty Principle, SRP)

  • 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야 함
  • 클래스가 제공하는 모든 기능은 이 책임과 주의 깊게 부합해야 함
  • 클래스를 변경하는 이유는 단 한가지여야 함

 

뭔가를 기록하기 위한 메모장(Journal)을 설계하고자 한다. 메모장에는 하나의 제목(title)과 여러 항목(entire)이 저장 될 수 있다.각 항목이 기입될 경우 이를 임시로 메모리에 저장할 수단(add), 비휘발성 장치에 저장하는 수단(save)이 필요하다. 이 요구사항에 맞추어 코드를 작성하면 아래와 같다.

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
#include<iostream>
#include<vector>
#include<string>
#include<fstream>
 
using namespace std;
 
class Journal {
public:
    string title;
    vector<string> entries;
 
    explicit Journal(const string& title) : title{ title } {}
    explicit Journal() : Journal("none") {}
 
    void add(const string& entry) {
        static int count = 1;
        entries.push_back(to_string(count+++ ": " + entry + "\n");
    }
 
    void save(const string& filename) {
        ofstream ofs(filename);
        for (string c : entries)
            ofs << c;        
    }
 
};
 
int main(void) {
    Journal j;
    j.add("Hello");
    j.add("World!");
    j.save("my title.txt");
 
    return 0;
}
cs

생성자로 제목을 설정하고, add함수로 entries를 채워놓고, save로 필드의 내용들을 파일에 저장한다.

save함수는 파일을 디스크에 저장하고 있는데, 만약 다른 경로나 원격 클라우드에 저장하는 기능도 추가해야한다면 어떻게 해야할까? 기존의 save함수에 저장매체를 구분지을 분기점을 형성하거나, 새로운 함수를 추가하면 될것이다.

하지만 위 방식에는 문제점이 하나 있다. 메모장의 책임은 메모 항목들을 기입/관리하는 것이지 디스크에 쓰는것이 아니다. 또한 이 경우 Journal 클래스를 수정하는 이유는 메모 항목 기입/관리와 저장방식의 수정 2가지로 나뉘게 된다. 즉, 단일책임 원칙에 위반된다.

이를 해결하기위해 Journal 클래스의 기능을 아래의 2가지로 분리한다.

  1. 메모 항목들을 기입/관리
  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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include<iostream>
#include<vector>
#include<string>
#include<fstream>
 
using namespace std;
 
class Journal {
public:
    string title;
    vector<string> entries;
 
public:
    explicit Journal(const string& title) : title{ title } {}
    explicit Journal() : Journal("none") {}
 
    void add(const string& entry) {
        static int count = 1;
        entries.push_back(to_string(count+++ ": " + entry);
    }
 
 
    const vector<string>& getEntries() const{
        return entries;
    }
 
};
 
class PersistenceManager {
public:
    static void disk_save(const Journal& j, const string& filename) {
        ofstream ofs(filename);
        for (auto& s : j.getEntries())
            ofs << s << endl;            
    }
 
    static void cloud_save(const Journal& j, const string& filename) {
        // codes
    }
};
 
int main(void) {
    Journal j;
    j.add("Hello");
    j.add("World!");
 
    PersistenceManager p;
    p.disk_save(j, "my file.txt");
 
    return 0;
}
cs

비록 PersistenceManager가 Journal에 의존적이게 설계되었지만, 역할에 따라 class를 2개로 분리하는 구현에 성공했다. 단일 책임 원칙이 의미하는 바가 정확히 이런것이다. 각 클래스는 단 한가지의 책임을 부여받아, 수정할 이유가 단 한가지여야 한다. 저장방식의 수정이 필요하더라도 Journal이 아닌 PersistenceManager만을 수정하면 된다.

 

SRP를 위배하는 안티 패턴의 극단적인 예로는 전지전능 패턴이 있다. 객체 하나에 많은 기능을 담은 괴물같은 클래스 이기에 다른 객체와의 호환이 매우 어렵다.

 

 

 

2. 열림-닫힘 원칙(Open-Closed Principle, OCP)

정의 : 타입이 확장에는 열려 있지만 수정에는 닫혀 있도록 강제하는 것

DB에 어떤 제품군에 대한 정보가 저장되어 있다고 하자. 개별 제품은 서로 다른 색상과 크기를 가지며 아래와 같이 정의된다.

1
2
3
4
5
6
7
8
9
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
 
class Product {
public:
    string name;
    Color color;
    Size size;
};
cs

이제 주어진 제품 집함을 조건에 따라 필터링하는 기능을 만들고 싶다고 하자.  다음과 같이 어떤 필터가 조건에 합치하는 제품들의 집합을 가지도록 구현할 수 있다.

1
2
3
4
5
6
class ProductFilter {
public:
    typedef vector<Product*> Items;
 
    ProductFilter::Items by_color(Items items, Color color);
};
cs

 

이제 필터링 조건으로 색상을 기준으로 삼는 필터를 만들려고 한다. 색상만을 기준으로 제품을 구분하는 멤버함수를 아래와 같이 정의할 수 있다.

1
2
3
4
5
6
7
ProductFilter::Items ProductFilter::by_color(Items items, Color color){
    Items result;
    for(auto& i : items)
        if(i->color == color)
            result.push_back(i);
    return result;
}
cs

 

이제 아래의 코드로 잘 작동 되는지 확인 할 수 있다.

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
46
47
48
49
50
51
52
53
54
55
#include<iostream>
#include<string>
#include<vector>
 
using namespace std;
 
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
 
class Product {
public:
    string name;
    Color color;
    Size size;
 
    Product(string name_, Color color_, Size size_) : name(name_), color(color_), size(size_)
    {    }
};
 
class ProductFilter {
public:
    typedef vector<Product*> Items;
 
    ProductFilter::Items by_color(Items items, Color color);
};
 
 
ProductFilter::Items ProductFilter::by_color(Items items, Color color) {
    Items result;
    for (auto& i : items)
        if (i->color == color)
            result.push_back(i);
    return result;
}
 
 
int main(void) {
    //vector<Product*> items;
    ProductFilter::Items items;
    items.push_back(new Product("pencil", Color::Red, Size::Small));
    items.push_back(new Product("apple", Color::Red, Size::Medium));
    items.push_back(new Product("eraser", Color::Blue, Size::Small));
    items.push_back(new Product("phone", Color::Blue, Size::Large));
    items.push_back(new Product("book", Color::Green, Size::Medium));
    items.push_back(new Product("tree", Color::Green, Size::Large));
 
    ProductFilter filter;
 
    //vector<Product*> ret;
    ProductFilter::Items ret;
    ret = filter.by_color(items, Color::Red);
    //ret 에는 색깔이 빨간 제품만 들어가게된다.
    
    return 0;
}
cs

 

items에 담긴 제품 중에 색깔이 빨간색인 제품만을 골라 ret에 담도록 한다.

이후에 색깔 뿐만 아니라 크기를 기준으로 한 필터링 기능도 구현해 달라는 요구사항이 들어온다면 어떻게 해야 하는가?? 그저 ProductFilter클래스의 by_color 함수를 조금 수정하면 될것이다. 색깔과 사이즈 2개 모두를 기준으로 필터링 하고 싶을 경우 단순히 매개변수에 Size를 추가하고 if에서 &&를 통해 조건을 추가하여 동시에 판별하도록 하면 충분하다.

 

하지만 기능 확장을 위해 정상 작동하는 클래스의 내부를 수정하고 있으며, 또한 함수의 이름은 by_color인데 색 뿐만 아니라 크기까지 명세로 삼는 필터 작업을 추가했으며, 결정적으로 코드에 대한 기억이 잊혀질때쯤 기능을 추가하거나 제거하려 할 경우 어디를 얼마나 수정할지 정확히 기억 안날 수 있다. 그러면 더 나은방법은 없을까?

여기서 필요한것이 열림-닫힘 원칙이다.

 

열림-닫힘 원칙은 타입이 확장에는 열려 있지만 수정에는 닫혀 있도록 강제하는 것이다.

즉, 기존 코드의 수정없이 필터링을 확장할 수 있는 방법이 필요하다.

 

우선 필터링 절차를 개념적으로 2개의 부분으로 나누어야 한다.(SRP 원칙)

첫 번째는 "필터"(항목 집합을 넘겨받아 그 일부를 리턴)이고 다른 하나는 "명세"(데이터 항목을 구분하기 위한 조건의 정의)이다.

 

"명세"는 아래와 같이 단순하게 정의 할 수 있다.

1
2
3
4
template <typename T> class Specification{
public:
    virtual bool is_satisfied(T* item) = 0;
};
cs

타입 T를 임의로 선택 가능하게끔 하여 전체적인 접근 방법을 재사용 가능하게 만든다.

 

이제 Specification을 기반으로 필터링을 수행할 방법이 필요하다.

1
2
3
4
5
template <typename T> class Filter {
public:
    virtual vector<T*> filter(vector<T*> items, Specification<T>& spec) = 0;
};
 
cs

Filter 클래스를 가상화 시키고 거르고 싶은 자료형을 템플릿에 집어 넣어 걸러 내도록 한다.

 

 

이 함수는 전체 항목 집합과 명세를 인자로 받아, 명세에 합치되는 항목들을 리턴한다. 여기서 항목들은 vector<T*>에 저장된다고 가정한다. 실제 환경에서는 좀더 유연하게 임의의 컬렉션 타입을 지원할 수 있도록, 반복자 또는 별도로 준비한 인터페이스를 filter에 넘길 수 있다.

(안타깝게도 C++는 나열타입이나 컬렉션 타입의 표준화에 실패했다. 다른언어는 그러한 타입을 표준적으로 지원하고 있어 이러한 고민을 덜어준다.)

 

이런 기반을 통해 아래처럼 개선된 필터를 구현 할 수 있다.

1
2
3
4
5
6
7
8
9
10
class BetterFilter : Filter<Product> {
public:
    vector<Product*> filter(vector<Product*> items, Specification<Product>& spec) override {
        vector<Product*> result;
        for(auto& p: items)
            if(spec.is_satisfied(p))
                result.push_back(p);
        return result;                
    }
};
cs

 

이제 색상 필터에 대한 명세를 작성한다.

1
2
3
4
5
6
7
8
9
class ColorSpecification : Specification<Product> {
public:
    Color color;
    explicit ColorSpecification(const Color color) : color{color} {}
 
    bool is_satisfied(Product* item) override {
    return item->color == color;
    }
}
cs

 

 

이제 아래와 같이 필터링 할 수 있다.

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include<iostream>
#include<string>
#include<vector>
 
using namespace std;
 
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
 
class Product {
public:
    string name;
    Color color;
    Size size;
 
    Product(string name_, Color color_, Size size_) : name(name_), color(color_), size(size_)
    {    }
};
 
 
template <typename T> class Specification {
public:
    virtual bool is_satisfied(T* item) = 0;
};
 
template <typename T> class Filter {
public:
    virtual vector<T*> filter(vector<T*> items, Specification<T>& spec) = 0;
};
 
 
class BetterFilter : public Filter<Product> {
public:
    vector<Product*> filter(vector<Product*> items, Specification<Product>& spec) override {
        vector<Product*> result;
        for (auto& p : items)
            if (spec.is_satisfied(p))
                result.push_back(p);
        return result;
    }
};
 
 
//색깔만으로 필터링 할때의 명세
class ColorSpecification : public Specification<Product> {
public:
    Color color;
    explicit ColorSpecification(const Color color) : color{ color } {}
 
    bool is_satisfied(Product* item) override {
        return item->color == color;
    }
};
 
 
int main(void) {
    Product apple{ "Apple", Color::Green, Size::Small };
    Product tree{ "Tree", Color::Green, Size::Large };
    Product house{ "House", Color::Blue, Size::Large };
 
    vector<Product*> all{ &apple, &tree, &house };
 
    BetterFilter bf;
    ColorSpecification green(Color::Green);
 
    auto green_things = bf.filter(all, green);
 
    return 0;
}
 
cs

 

 

색깔과 크기를 동시에 필터링 하고싶다면 어떠헤 해야 하는가? 복합(composite)명세를 만들면 된다.

예를 들어 AND 논리 연산으로 명세를 조합해야 한다면 아래와 같이 복합 명세를 정의하여 이용한다.

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#include<iostream>
#include<string>
#include<vector>
 
using namespace std;
 
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
 
class Product {
public:
    string name;
    Color color;
    Size size;
 
    Product(string name_, Color color_, Size size_) : name(name_), color(color_), size(size_)
    {    }
};
 
 
template <typename T>
class Specification {
public:
    virtual bool is_satisfied(T* item) = 0;
};
 
template <typename T>
class Filter {
public:
    virtual vector<T*> filter(vector<T*> items, Specification<T>& spec) = 0;
};
 
 
class BetterFilter : public Filter<Product> {
public:
    vector<Product*> filter(vector<Product*> items, Specification<Product>& spec) override {
        vector<Product*> result;
        for (auto& p : items)
            if (spec.is_satisfied(p))
                result.push_back(p);
        return result;
    }
};
 
 
//색깔만으로 필터링 할때의 명세
class ColorSpecification : public Specification<Product> {
public:
    Color color;
    explicit ColorSpecification(const Color color) : color{ color } {}
 
    bool is_satisfied(Product* item) override {
        return item->color == color;
    }
};
 
class SizeSpecification : public Specification<Product> {
public:
    Size size;
    explicit SizeSpecification(const Size size) : sizesize } {}
 
    bool is_satisfied(Product* item) override {
        return item->size == size;
    }
};
 
 
//색깔과 크기 모두로 필터링 할때의 명세
template<typename T>
class AndSpecification : public Specification<T> {
public:
    Specification<T>& first;
    Specification<T>& second;
 
    AndSpecification(Specification<T>& first, Specification<T>& second) : first( first ), second( second ) {}
 
    bool is_satisfied(T* item) override {
        return first.is_satisfied(item) && second.is_satisfied(item);
    }
};
 
 
int main(void) {
    Product apple{ "Apple", Color::Green, Size::Small };
    Product tree{ "Tree", Color::Green, Size::Large };
    Product house{ "House", Color::Blue, Size::Large };
 
    vector<Product*> all{ &apple, &tree, &house };
 
    BetterFilter bf;
    SizeSpecification large(Size::Large);
    ColorSpecification green(Color::Green);
    AndSpecification<Product> green_and_large{ large, green };
 
    auto green_things = bf.filter(all, green);
    auto big_green_things = bf.filter(all, green_and_large);
 
 
    return 0;
}
cs

 

단순히 명세를 하나 추가함에도 불구하고 적지않은 코드를 추가해야한다. 보다 단순히 하기 위해 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
#include<iostream>
#include<string>
#include<vector>
 
using namespace std;
 
enum class Color { Red, Green, Blue };
enum class Size { Small, Medium, Large };
 
class Product {
public:
    string name;
    Color color;
    Size size;
 
    Product(string name_, Color color_, Size size_) : name(name_), color(color_), size(size_)
    {    }
};
 
template<typename T>
class AndSpecification;
 
template <typename T>
class Specification {
public:
    virtual bool is_satisfied(T* item) = 0;
 
    AndSpecification<T> operator &&(Specification&& other) {
        return AndSpecification<T>(*this, other);
    }
};
 
template <typename T>
class Filter {
public:
    virtual vector<T*> filter(vector<T*> items, Specification<T>& spec) = 0;
};
 
 
class BetterFilter : public Filter<Product> {
public:
    vector<Product*> filter(vector<Product*> items, Specification<Product>& spec) override {
        vector<Product*> result;
        for (auto& p : items)
            if (spec.is_satisfied(p))
                result.push_back(p);
        return result;
    }
};
 
 
//색깔만으로 필터링 할때의 명세
class ColorSpecification : public Specification<Product> {
public:
    Color color;
    explicit ColorSpecification(const Color color) : color{ color } {}
 
    bool is_satisfied(Product* item) override {
        return item->color == color;
    }
};
 
class SizeSpecification : public Specification<Product> {
public:
    Size size;
    explicit SizeSpecification(const Size size) : sizesize } {}
 
    bool is_satisfied(Product* item) override {
        return item->size == size;
    }
};
 
 
//색깔과 크기 모두로 필터링 할때의 명세
template<typename T>
class AndSpecification : public Specification<T> {
public:
    Specification<T>& first;
    Specification<T>& second;
 
    AndSpecification(Specification<T>& first, Specification<T>& second) : first(first), second(second) {}
 
    bool is_satisfied(T* item) override {
        return first.is_satisfied(item) && second.is_satisfied(item);
    }
};
 
 
int main(void) {
    Product apple{ "Apple", Color::Green, Size::Small };
    Product tree{ "Tree", Color::Green, Size::Large };
    Product house{ "House", Color::Blue, Size::Large };
 
    vector<Product*> all{ &apple, &tree, &house };
 
    BetterFilter bf;
    SizeSpecification large(Size::Large);
    ColorSpecification green(Color::Green);
    //AndSpecification<Product> green_and_large{ large, green };
 
    auto green_things = bf.filter(all, green);
    //auto big_green_things = bf.filter(all, green_and_large);
    auto green_and_big = ColorSpecification(Color::Green) && SizeSpecification(Size::Large);
    auto big_green_things = bf.filter(all, green_and_big);
    
    return 0;
}
 
cs

 

결코 이해하기 쉽지 않은 개념이긴하다. 거르고 싶은 원본 목록과 걸러야할 기준까지 전부다 객체로 넘기는 방식이라 보면 이해하기 간편할것이다.

 

Filter 객체가 vector<T>를 통해 내용물의 원본과 Specification으로 거르고 싶은 명세를 전달 받으면 그 명세의 기준에 맞추어 제품들을 걸러 내게 된다.

Filter에서는 vector<T> 요소들과 Specification의 내부에 대해 알 수 없으므로 둘 사이의 실질적은 비교는 외부에서 행해져야 한다.

class enum을 통해 각 객체에서 접근 가능한 열거형을 만들어 준후 이 값들을 기준으로 걸러내게끔 해야한다. 그리고 Specification을 상속받은 각 명세를 만들어 준 후 '무엇'을 '어떻게' 거를지를 결정한다.

 

class 어떻게_Specification : public Specification<무엇을>
class 색깔로_Specification : public Specification<Product를>

 

 

즉 실질적인 비교는 '명세(specification)' 오브젝트 내에서 이루어 지며 명세와 정확한 형 지정을 통해 상속받은 상품 사이에는 비교를 위한 최소한의 약속이 존재해야한다. 즉, 어떻게_Specification의 설계는 '무엇을'을 기준으로 이루어 져야 한다.

 

이런식으로 객체지향적으로 필터를 설계 할 수 있다.

 

 

 

여기서 어떻게 OCP의 원칙이 강제되고 있을까?

OCP는 기존에 작성하고 테스트했던 코드에 다시 손을 대는일이 있어서는 안된다는 것을 의미한다.

이 예는 Specification<T>와 Filter<T> 를 따로 만들었기에 filter 내부를 손대지 않고 specification의 자식 클래스를 추가하는것으로 새로운 필터링을 추가 할 수 있다.

바로 이것이

"확장에는 열려있지만 수정에는 닫혀 있다."의 의미이다.

 

 

 

Comments