스트래티지 패턴이란 |
Strategy : 전략
전략 패턴이라고도 불림
알고리즘/함수를 캡슐화하여 동적으로 교체할 수 있도록 하는 패턴
Client와 독립적으로 알고리즘/함수를 변경할 수 있다.
스트래티지 패턴의 구조 |
1) Strategy
- 원본 전략과 알고리즘을 변경할 수 있는 setter를 정의하고, 변경될 부분은 Concrete클래스들에 위임
2) ConcreteStrategy
- 제공된 인터페이스를 구현하고 알고리즘을 구체화
3) Context
- Context가 전략객체를 사용해서 알고리즘을 실행
4) Client
- Client가 Context객체를 생성하고 적절한 Strategy를 설정
스트래티지 패턴이 없다면 |
원본 전략인 Strategy가 있다.
showName() : 이름출력
math() : 수학관련 함수
sort() : 버블소트
search() : 선형탐색
으로 구성된 멤버함수들이 있다고 해보자.
새로운 전략이 필요해서 전략 A를 작성한다.
원본 전략인 Strategy를 상속받되 showName()함수만 오버라이딩해서 이름만 변경했다.
그 외에 math(), sort(), search()함수는 그대로 오버라이딩해서 원본 전략과 동일하다.
새로운 전략 B를 작성한다.
showName()을 오버라이딩해서 이름을 변경했고
math()함수는 오버라이딩 하지 않고 그대로 상속받았고
sort()함수는 오버라이딩해서 퀵소트로 변경했다.
그런데 전략B에서는 search()는 아무것도 하지 않아야 해서 일부러 search()를 오버라이딩해서 함수 내용일 비워서 아무것도 하지 않도록 구현했다.
또 새로운 전략 C를 구현하는데 showName()과 search()는 독자적으로 구현했지만
sort()를 전략 B와 동일한 퀵소트로 구현했다.
이렇게 되면 전략B와 전략C에서 동일한 코드를 중복해서 작성하게 됐다.
새로운 전략D에서는 search()함수가 아무것도 하면 안돼서 전략B와 마찬가지로 굳이 오버라이딩해서 함수 내용을 비워뒀다. 이 또한 중복 코드가 발생한것과 마찬가지이다.
결론적으로 코드의 중복도 발생하고, 각 전략은 상속받은 후 혹은 구현 후 다른 알고리즘으로 교체시 OCP원칙을 위배하게 되며 코드의 재사용을 위해 상속을 사용했지만 정작 대부분 새로 작성되어 상속의 효율이 떨어진다.
스트래티지 패턴을 사용한다면 |
스트래티지 패턴을 사용하면 알고리즘을 캡슐화하고 런타임에 알고리즘을 교체할 수 있게 되어 위에서 발생한 문제들을 해결 가능하다.
위에서 변경되었던 sort()와 search()를 캡슐화시킨다.
sort의 경우 SortInterface를 구현하고 버블정렬, 병합정렬, 퀵정렬 등이 SortInterface를 상속받아 구현한다.
이 경우 퀵정렬을 변경해도 다른 정렬은 영향을 받지 않으며, 해당 정렬객체를 사용하면 되므로 소스코드의 중복도 발생하지 않는다.
또한 Client는 B전략이 무슨 정렬을 사용하는지 몰라도 사용이 가능하다.
또한 런타임에 알고리즘을 교체할수도 있다.
#include <iostream>
using namespace std;
/* 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; }
};
/* Search함수를 클래스로 작성 */
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
{
protected:
// 인터페이스 형식의 레퍼런스 변수
SortInterface* sortInterface = nullptr;
SearchInterface* searchInterface = nullptr;
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(); } // 탐색 인터페이스에 위임
};
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; }
};
void main()
{
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();
}
장단점 |
1) 장점
- OCP 준수 : Context나 원본전략이나 그외 전략들을 변경하지 않아도 새로운 전략을 추가할 수 있음
- 알고리즘을 캡슐화하므로 런타임에 알고리즘을 교체할수 있고 다른 컨텍스트에서도 재사용 가능
- 알고리즘을 각각 독립적으로 구현하기 좋음
2) 단점
- 알고리즘별로 클래스가 하나씩 생기므로 클래스 수가 증가함
- 런타임에 알고리즘을 교체하는것은 오버헤드가 발생할 수 있음
- 알고리즘의 종류가 적다면 쓸데없이 코드만 증가함
※ 참고 문헌