본문 바로가기
개발/Architecture & Design Pattern

C++ - 전략, 스트래티지 패턴 (Strategy Pattern)

by 피로물든딸기 2024. 1. 29.
반응형

C, C++ 전체 링크

Architecture & Design Pattern 전체 링크

 

참고

- 클래스 다이어그램 그리기

- 인터페이스 vs 추상 클래스 (Java, C++ 비교)

- 스마트 포인터 : unique_ptr

 

전략, 스트래티지 패턴 (Strategy  Pattern) - 행동 패턴

- 알고리즘을 캡슐화하여 동적으로 교체할 수 있도록 하는 패턴

- 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다.

 

구현

- 전략 패턴에서 사용할 각 알고리즘에 대한 인터페이스를 정의 (전략 인터페이스, Strategy Interface)

- 정의된 인터페이스를 구현하여 알고리즘을 구체화하는 전략 클래스 작성 (구체적인 전략, Concrete Strategies)

- 컨텍스트 클래스에 전략 객체를 사용하여 알고리즘을 실행 (컨텍스트, Context)

- 컨텍스트 객체를 생성하고 필요한 전략을 설정 (클라이언트, Client)

 

장점

- OCP(개방-폐쇄 원칙), 컨텍스트를 변경하지 않아도 새로운 전략을 추가할 수 있다.

- 알고리즘을 캡슐화하기 때문에 런타임에 알고리즘을 교체할 수 있다. (유연성, Flexibility)

- 알고리즘을 독립적으로 구현하고 캡슐화하므로, 각각의 알고리즘을 이해하고 수정하기가 쉽다.

- 알고리즙을 캡슐화해서 다른 컨텍스트에서 재사용할 수 있다. (재사용성, Reusability)

- 상속 대신 구성(composition)을 활용한다.

 

단점

- 각 알고리즘에 대한 클래스를 만들어야 하므로 클래스 수가 증가한다.

- 런타임에 알고리즘을 교체하는 경우는 오버헤드가 발생할 수 있다.

- 전략 클래스와 컨텍스트 클래스 간의 의존성이 증가할 수 있다.

- 알고리즘이 많이 없다면 클래스와 인터페이스만 복잡해진다.


알고리즘 전략 클래스

 

알고리즘 대회 문제를 풀기 위해 알고리즘 전략 클래스를 만든다고 가정하자.

 

Strategy 클래스

- showName() : 전략마다 이름이 다르므로 상속받는 자식 클래스에서 구현

- math() : 모든 클래스는 공통된 math 클래스를 사용

- sort() : 기본 정렬 알고리즘, 필요하면 각 클래스에서 오버라이딩

 

A 전략(= Context)기본 정렬(상속 없음) 알고리즘을 사용하고, 

B 전략퀵 정렬을 사용하도록 오버라이딩 하였다.

 

코드로 구현하면 다음과 같다.

#include <iostream>

using namespace std;

class Strategy
{
public:
	virtual ~Strategy() = default;
	virtual void showName() = 0; // 각 전략 이름은 자식 클래스에서 구현
	void math() { cout << "math" << endl; } // 모든 클래스는 math 메서드를 사용

	// 기본 알고리즘을 사용하되, 자식에서 필요한 알고리즘을 구현할 수도 있다.
	void sort() { cout << "basic sort" << endl; } // 기본 정렬
};

class StrategyA : public Strategy
{
public:
	void showName() { cout << "showName A" << endl; }
};

class StrategyB : public Strategy
{
public:
	void showName() { cout << "showName B" << endl; }
	void sort() { cout << "quick sort" << endl; }
};

int main(void)
{
	StrategyA* a = new StrategyA();
	a->showName();
	a->sort();

	cout << endl;

	StrategyB* b = new StrategyB();
	b->showName();
	b->sort();

	return 0;
}


요구사항 변경

 

문제를 더 잘 풀기 위해 "탐색" 알고리즘(search)이 추가되었다고 가정하자.

 

그런데 B 전략 퀵 정렬로 충분하기 때문에 탐색 알고리즘이 불필요하다.

따라서 B 전략탐색 알고리즘을 굳이 오버라이딩해서 아무것도 하지 않도록 구현해야 한다.

 

그리고 퀵 정렬을 사용하고 이분 탐색을 쓰는 C 전략아무것도 하지 않고 포기하는 D 전략이 추가되었다.

 

상속으로 인해 서브 클래스 B와 C의 sort() / 클래스 B와 D의 search()코드가 중복된다.

여기서 B와 C는 search()는 사용하지 않도록 구현하였다.

그리고 상속으로 인해 각 전략의 행동을 파악하기가 힘들고 코드 변경 시 사이드 이펙트가 발생할 수 있다.

또한 각 전략은 한 번 알고리즘을 상속받은 후, 다른 알고리즘으로 변경할 수가 없다.

 

즉, 코드를 재사용하기 위해 상속을 사용했지만, 큰 효과가 없다.

 

C++로 구현하면 다음과 같다.

#include <iostream>

using namespace std;

class Strategy
{
public:
	virtual ~Strategy() = default;
	virtual void showName() = 0; // 각 전략 이름은 자식 클래스에서 구현
	void math() { cout << "math" << endl; } // 모든 클래스는 math 메서드를 사용

	// 기본 알고리즘을 사용하되, 자식에서 필요한 알고리즘을 구현할 수도 있다.
	void sort() { cout << "basic sort" << endl; } // 기본 정렬
	void search() { cout << "basic search" << endl; }
};

class StrategyA : public Strategy
{
public:
	void showName() { cout << "showName A" << endl; }
};

class StrategyB : public Strategy
{
public:
	void showName() { cout << "showName B" << endl; }
	void sort() { cout << "quick sort" << endl; }
	void search() { cout << "search nothing" << endl; }
};

class StrategyC : public Strategy
{
public:
	void showName() { cout << "showName C" << endl; }
	void sort() { cout << "quick sort" << endl; }
	void search() { cout << "binary search" << endl; }
};

class StrategyD : public Strategy
{
public:
	void showName() { cout << "showName D" << endl; }
	void sort() { cout << "sort nothing" << endl; }
	void search() { cout << "search nothing" << endl; }
};

int main(void)
{
	StrategyA* a = new StrategyA();
	a->showName();
	a->sort();
	a->search();
	
	cout << endl;

	StrategyB* b = new StrategyB();
	b->showName();
	b->sort();
	b->search();

	cout << endl;

	StrategyC* c = new StrategyC();
	c->showName();
	c->sort();
	c->search();

	cout << endl;

	StrategyD* d = new StrategyD();
	d->showName();
	d->sort();
	d->search();

	return 0;
}

 

그리고 아래 코드가 중복된다.

void sort() { cout << "quick sort" << endl; }
void search() { cout << "search nothing" << endl; }

인터페이스

 

좀 더 코드를 개선하기 위해 sort()를 인터페이스로 구현해 보자.

class SortInterface
{
public:
	virtual ~SortInterface() = default;
	virtual void sort() = 0;
};

 

Strategy 클래스에서 sort는 삭제한다.

그리고 D 전략은 sort()를 사용하지 않기 때문에 구현하지 않고,

A, B, CSort 인터페이스를 상속해서 sort()를 구현한다.

...

class StrategyC : public Strategy, SortInterface
{
public:
	void showName() { cout << "showName C" << endl; }
	void sort() { cout << "quick sort" << endl; }
	void search() { cout << "binary search" << endl; }
};

class StrategyD : public Strategy
{
public:
	void showName() { cout << "showName D" << endl; }
	//void sort() { cout << "sort nothing" << endl; }
	void search() { cout << "search nothing" << endl; }
};

 

그림으로 그려보면 다음과 같다.

 

그리고 main에서 sort()를 사용하지 않는 클래스는 주석처리 해야 한다.

	StrategyD* d = new StrategyD();
	d->showName();
	//d->sort();
	d->search();

 

인터페이스를 구현하도록 해서 sort가 불필요한 클래스는 코드를 작성하지 않아도 되지만,

sort 구현에 대해 중복이 발생할 수 있고(B, C = quick),

sort에 대한 요구사항이 바뀌면 구현된 서브 클래스를 모두 수정해야 한다.

또한 여전히 실행 후, 알고리즘을 변경할 수 없다.

 

전체 코드는 다음과 같다.

#include <iostream>

using namespace std;

class SortInterface
{
public:
	virtual ~SortInterface() = default;
	virtual void sort() = 0;
};

/* ------------------------------------------------------------------- */

class Strategy
{
public:
	virtual ~Strategy() = default;
	virtual void showName() = 0; // 각 전략 이름은 자식 클래스에서 구현
	void math() { cout << "math" << endl; } // 모든 클래스는 math 메서드를 사용

	// 기본 알고리즘을 사용하되, 자식에서 필요한 알고리즘을 구현할 수도 있다.
	//void sort() { cout << "basic sort" << endl; } // -> Interface
	void search() { cout << "basic search" << endl; }
};

class StrategyA : public Strategy, SortInterface
{
public:
	void showName() { cout << "showName A" << endl; }
	void sort() { cout << "basic sort" << endl; }
};

class StrategyB : public Strategy, SortInterface
{
public:
	void showName() { cout << "showName B" << endl; }
	void sort() { cout << "quick sort" << endl; }
	void search() { cout << "search nothing" << endl; }
};

class StrategyC : public Strategy, SortInterface
{
public:
	void showName() { cout << "showName C" << endl; }
	void sort() { cout << "quick sort" << endl; }
	void search() { cout << "binary search" << endl; }
};

class StrategyD : public Strategy
{
public:
	void showName() { cout << "showName D" << endl; }
	//void sort() { cout << "sort nothing" << endl; }
	void search() { cout << "search nothing" << endl; }
};

int main(void)
{
	StrategyA* a = new StrategyA();
	a->showName();
	a->sort();
	a->search();

	cout << endl;

	StrategyB* b = new StrategyB();
	b->showName();
	b->sort();
	b->search();

	cout << endl;

	StrategyC* c = new StrategyC();
	c->showName();
	c->sort();
	c->search();

	cout << endl;

	StrategyD* d = new StrategyD();
	d->showName();
	//d->sort();
	d->search();

	return 0;
}


스트래티지 패턴 (Strategy Pattern)

 

strategy 패턴을 사용하면 알고리즘을 캡슐화하고 런타임에 교체가 가능하다.

 

sortsearch바뀌는 부분이기 때문에 따로 캡슐화시킨다.

그러면 quick sort가 변경되더라도 다른 sort는 영향을 미치지 않게 될 것이다.

따라서 Strategy 클래스에서는 해당 알고리즘을 구체적으로 구현하는 방법에 대해서는 알 필요가 없다.

 

sort()와 search()는 인터페이스(Sort, Search)로 표현하고, 구체적인 행동을 구현할 때 인터페이스를 구현한다.

예를 들어 Sort 인터페이스를 구현하고, 각 sort()는 인터페이스를 상속받아 구현한다.

class SortInterface
{
public:
	virtual ~SortInterface() = default;
	virtual void sort() = 0;
};

class NoSort : public SortInterface
{
public:
	void sort() override { cout << "sort nothing" << endl; }
};

class BasicSort : public SortInterface
{
public:
	void sort() override { cout << "basic sort" << endl; }
};

class QuickSort : public SortInterface
{
public:
	void sort() override { cout << "quick sort" << endl; }
};

 

이 방식을 이용하면 B 전략 quick 정렬을 사용하는 것을 몰라도

Strategy 클래스를 상속하는 모든 클래스는 sort()만 호출하면 된다.

 

그리고 C 전략quick 정렬을 사용하면 QuickSort를 재사용할 수 있다.

정렬과 탐색은 인스턴스가 만들어질 때 결정하기 위해 생성자에서 할당하면 된다.

class StrategyB : public Strategy
{
public:
	StrategyB() 
	{
		sortInterface = new QuickSort();
		searchInterface = new NoSearch();
	}

	void showName() { cout << "showName B" << endl; }
};

class StrategyC : public Strategy
{
public:
	StrategyC() 
	{
		sortInterface = new QuickSort();
		searchInterface = new BinarySearch();
	}

	void showName() { cout << "showName C" << endl; }
};

 

여기서 인터페이스를 교체할 수 있는 set 함수를 제공하여 필요한 경우 알고리즘을 런타임에 교체할 수 있다.

class Strategy
{
public:
	...
   
	void setSort(SortInterface* si) { sortInterface = si; }
	void setSearch(SearchInterface* si) { searchInterface = si; }

protected:
	SortInterface* sortInterface = nullptr; 
	SearchInterface* searchInterface = nullptr;
};

 

예시 코드는 다음과 같다.

	StrategyD* d = new StrategyD();
	d->showName();
	d->sort();
	d->search();

	d->setSort(new BasicSort());
	d->setSearch(new BinarySearch());

	d->sort();
	d->search();

 

전체 코드는 다음과 같다.

#include <iostream>

using namespace std;

class SortInterface
{
public:
	virtual ~SortInterface() = default;
	virtual void sort() = 0;
};

class NoSort : public SortInterface
{
public:
	void sort() override { cout << "sort nothing" << endl; }
};

class BasicSort : public SortInterface
{
public:
	void sort() override { cout << "basic sort" << endl; }
};

class QuickSort : public SortInterface
{
public:
	void sort() override { cout << "quick sort" << endl; }
};

class SearchInterface
{
public:
	virtual ~SearchInterface() = default;
	virtual void search() = 0;
};

class NoSearch : public SearchInterface
{
public:
	void search() override { cout << "search nothing" << endl; }
};

class BasicSearch : public SearchInterface
{
public:
	void search() override { cout << "basic search" << endl; }
};

class BinarySearch : public SearchInterface
{
public:
	void search() override { cout << "binary search" << endl; }
};

/* ------------------------------------------------------------------- */

class Strategy
{
public:
	virtual ~Strategy() = default;
	virtual void showName() = 0; // 각 전략 이름은 자식 클래스에서 구현
	void math() { cout << "math" << endl; } // 모든 클래스는 math 메서드를 사용

	// 런타임에 알고리즘을 교체하기 위한 메서드 제공
	void setSort(SortInterface* si) { sortInterface = si; } 
	void setSearch(SearchInterface* si) { searchInterface = si; }

	void sort() { sortInterface->sort(); } // 정렬 인터페이스에 위임
	void search() { searchInterface->search(); } // 탐색 인터페이스에 위임
protected:
	// 인터페이스 형식의 레퍼런스 변수 
	SortInterface* sortInterface = nullptr; 
	SearchInterface* searchInterface = nullptr;
};

class StrategyA : public Strategy
{
public:
	StrategyA() 
	{
		sortInterface = new BasicSort();
		searchInterface = new BasicSearch();
	}

	void showName() { cout << "showName A" << endl; }
};

class StrategyB : public Strategy
{
public:
	StrategyB() 
	{
		sortInterface = new QuickSort();
		searchInterface = new NoSearch();
	}

	void showName() { cout << "showName B" << endl; }
};

class StrategyC : public Strategy
{
public:
	StrategyC() 
	{
		sortInterface = new QuickSort();
		searchInterface = new BinarySearch();
	}

	void showName() { cout << "showName C" << endl; }
};

class StrategyD : public Strategy
{
public:
	StrategyD()
	{
		sortInterface = new NoSort();
		searchInterface = new NoSearch();
	}

	void showName() { cout << "showName D" << endl; }
};

int main(void)
{
	StrategyA* a = new StrategyA();
	a->showName();
	a->sort();
	a->search();
	
	cout << endl;

	StrategyB* b = new StrategyB();
	b->showName();
	b->sort();
	b->search();

	cout << endl;

	StrategyC* c = new StrategyC();
	c->showName();
	c->sort();
	c->search();

	cout << endl;

	StrategyD* d = new StrategyD();
	d->showName();
	d->sort();
	d->search();

	d->setSort(new BasicSort());
	d->setSearch(new BinarySearch());

	d->sort();
	d->search();

	return 0;
}


unique pointer를 이용한 코드 개선

 

스마트 포인터 중 하나인 unique pointer를 이용해서 위의 코드를 개선하면 아래와 같다.

유니크 포인터는 해당 자원에 대한 유일한 소유권을 가진다.

 

정렬과 탐색 알고리즘을 교체할 때, new를 이용해 객체를 생성하였으나,

해당 알고리즘은 매번 새로 만들 필요가 없기 때문에 하나의 자원으로 간편하게 관리할 수 있다.

BaisicSort의 수명 StrategyD 인스턴스의 수명에 의존하게 되어 메모리 누수를 방지할 수 있다.

StrategyD* d = new StrategyD();

d->setSort(new BasicSort()); // new 불필요
ㄴ d->setSort(make_unique<BasicSort>());

 

먼저 인터페이스 레퍼런스 변수를 unique_ptr로 선언한다. (memory header 추가)

#include <memory>

class Strategy
{
...
protected:
	// 인터페이스 형식의 레퍼런스 변수 
	unique_ptr<SortInterface> sortInterface = nullptr; 
	unique_ptr<SearchInterface> searchInterface = nullptr;
};

 

set 메서드에서 move를 이용해 유니크 포인터의 소유권을 이동한다.

	// 런타임에 알고리즘을 교체하기 위한 메서드 제공
	void setSort(unique_ptr<SortInterface> si) { sortInterface = move(si); }
	void setSearch(unique_ptr<SearchInterface> si) { searchInterface = move(si); }

 

각 서브 클래스의 생성자에서는 make_unique를 이용해 객체를 안전하게 생성하도록 수정한다.

class StrategyA : public Strategy
{
public:
	StrategyA() 
	{
		sortInterface = make_unique<BasicSort>();
		searchInterface = make_unique<BasicSearch>();
	}

	...
};

 

전체 코드는 다음과 같다.

#include <iostream>
#include <memory>

using namespace std;

class SortInterface
{
public:
	virtual ~SortInterface() = default;
	virtual void sort() = 0;
};

class NoSort : public SortInterface
{
public:
	void sort() override { cout << "sort nothing" << endl; }
};

class BasicSort : public SortInterface
{
public:
	void sort() override { cout << "basic sort" << endl; }
};

class QuickSort : public SortInterface
{
public:
	void sort() override { cout << "quick sort" << endl; }
};

class SearchInterface
{
public:
	virtual ~SearchInterface() = default;
	virtual void search() = 0;
};

class NoSearch : public SearchInterface
{
public:
	void search() override { cout << "search nothing" << endl; }
};

class BasicSearch : public SearchInterface
{
public:
	void search() override { cout << "basic search" << endl; }
};

class BinarySearch : public SearchInterface
{
public:
	void search() override { cout << "binary search" << endl; }
};

/* ------------------------------------------------------------------- */

class Strategy
{
public:
    virtual ~Strategy() = default;
	virtual void showName() = 0; // 각 전략 이름은 자식 클래스에서 구현
	void math() { cout << "math" << endl; } // 모든 클래스는 math 메서드를 사용

	// 런타임에 알고리즘을 교체하기 위한 메서드 제공
	void setSort(unique_ptr<SortInterface> si) { sortInterface = move(si); }
	void setSearch(unique_ptr<SearchInterface> si) { searchInterface = move(si); }

	void sort() { sortInterface->sort(); } // 정렬 인터페이스에 위임
	void search() { searchInterface->search(); } // 탐색 인터페이스에 위임
protected:
	// 인터페이스 형식의 레퍼런스 변수 
	unique_ptr<SortInterface> sortInterface = nullptr; 
	unique_ptr<SearchInterface> searchInterface = nullptr;
};

class StrategyA : public Strategy
{
public:
	StrategyA() 
	{
		sortInterface = make_unique<BasicSort>();
		searchInterface = make_unique<BasicSearch>();
	}

	void showName() { cout << "showName A" << endl; }
};

class StrategyB : public Strategy
{
public:
	StrategyB() 
	{
		sortInterface = make_unique<QuickSort>();
		searchInterface = make_unique<NoSearch>();
	}

	void showName() { cout << "showName B" << endl; }
};

class StrategyC : public Strategy
{
public:
	StrategyC() 
	{
		sortInterface = make_unique<QuickSort>();
		searchInterface = make_unique<BinarySearch>();
	}

	void showName() { cout << "showName C" << endl; }
};

class StrategyD : public Strategy
{
public:
	StrategyD()
	{
		sortInterface = make_unique<NoSort>();
		searchInterface = make_unique<NoSearch>();
	}

	void showName() { cout << "showName D" << endl; }
};

int main(void)
{
	StrategyA* a = new StrategyA();
	a->showName();
	a->sort();
	a->search();
	
	cout << endl;

	StrategyB* b = new StrategyB();
	b->showName();
	b->sort();
	b->search();

	cout << endl;

	StrategyC* c = new StrategyC();
	c->showName();
	c->sort();
	c->search();

	cout << endl;

	StrategyD* d = new StrategyD();
	d->showName();
	d->sort();
	d->search();

	d->setSort(make_unique<BasicSort>());
	d->setSearch(make_unique<BinarySearch>());

	d->sort();
	d->search();

	return 0;
}

 

Client는 Sort와 Search를 가진다. (Has)

A, B, C, D 전략은 Strategy 클래스를 상속한다. (Extends)

xxSort, yySearch는 Sort와 Search를 구현한다. (Implements)


ClassDiagram.md

```mermaid
  classDiagram    
    class Strategy { 
      +showName()*
      +math()
      +search()
    }
    class A { 
      +showName()
      +sort()
    }
    class B { 
      +showName()
      +sort()
      +search()
    }
    class C { 
      +showName()
      +sort()
      +search()
    }
    class D { 
      +showName()
      // +sort()
      +search()
    }
    
    class Sort {
      +sort()*
    }

    <<Interface>> Sort

    Strategy <|-- A
    Strategy <|-- B    
    Strategy <|-- C
    Strategy <|-- D    
    
    Sort <|.. B    
    Sort <|.. C
```

 

```mermaid
  classDiagram    
    namespace Client {
      class Strategy { 
        SortInterface sortInterface*
        SearchInterface searchInterface*
        +showName()*
        +math()
        +setSort()
        +setSearch()
        +sort()
        +search()
      }
      class A { 
        +showName()
      }
      class B { 
        +showName()
      }
      class C { 
        +showName()
      }
      class D { 
        +showName()
      }
    }
    namespace Encapsulated_Sort {
      class Sort {
        +sort()*
      }
      class NoSort {
        sort()
      }
      class BasicSort {
        sort()
      }
      class Quick Sort {
        sort()
      }
    }
    namespace Encapsulated_Search {
      class Search {
        +search()*
      }
      class NoSearch {
        search()
      }
      class BasicSearch {
        search()
      }
      class BinarySearch {
        search()
      }
    }
    


    <<Interface>> Sort
    <<Interface>> Search
    
    Strategy <|-- A
    Strategy <|-- B    
    Strategy <|-- C
    Strategy <|-- D    
    
    Strategy --> Sort :  Sort Interface 
    Strategy --> Search : Search Interface 

    Sort <|.. NoSort 
    Sort <|.. BasicSort 
    Sort <|.. QuickSort

    Search <|.. NoSearch 
    Search <|.. BasicSearch 
    Search <|.. BinarySearch
    
```
반응형

댓글