디자인 패턴: 전략 패턴(Strategy Pattern)
이 글은 제 개인적인 공부를 위해 작성한 글입니다. 틀린 내용이 있을 수 있고, 피드백은 환영합니다.
개요
개발을 하다 보면 하나의 목적을 가진 알고리즘이나 로직이 상황에 따라 다르게 동작해야 할 때가 있다. 예를 들어 게임의 캐릭터가 무기에 따라 공격 방식을 바꾸거나, 상황에 따라 캐릭터의 입력 방식이 달라지는 경우가 있을 수 있다.
이때 가장 쉽게 선택하는 방식이 if-else 문이나 switch-case 문이다. 하지만 분기문이 늘어날수록 코드는 거대해지고 유지보수는 어려워진다.
전략 패턴?
전략 패턴은 디자인 패턴 중 행위 패턴에 속한다. 동일한 계열의 알고리즘들을 정의하고, 각 알고리즘을 개별 클래스로 캡슐화하여 이들을 상호 교환이 가능하도록 만드는 패턴이다. 전략 패턴을 사용하면 클라이언트의 시점과 상관없이 독립적으로 알고리즘을 변경할 수 있다.
쉽게 말해, 무엇을 하는가(인터페이스)와 어떻게 하는가(구체적인 구현)를 분리하여 실행 중에 원하는 방식(전략)을 선택할 수 있도록 하는 구조이다. 이건 SOLID 원칙 중 OCP(개방 폐쇄 원칙)과 DIP(의존역전 원칙)을 잘 지키는 설계 방식이기도 하다.
전략 패턴의 특징을 다시 정리하면 아래와 같다.
- 알고리즘 캡슐화 : 알고리즘을 독립적인 클래스로 캡슐화하여 교체가 용이
- 유연성 제공 : 런타임에 알고리즘을 동적으로 변경 가능
- 클래스 분리 : 클라이언트 코드와 알고리즘 클래스를 분리하여 코드의 가독성과 유지보수성을 높임
구조
전략 패턴은 크게 세 가지 구성 요소로 이루어진다.
- 전략(Strategy) : 외부에서 실행 가능한 알고리즘을 정의하는 인터페이스 또는 추상 클래스
- 구체적인 전략(Concrete Strategy) : 인터페이스를 상속받아 실제 알고리즘을 구현한 클래스
- 문맥(Context) : 전략을 사용하는 주체. 구체적인 알고리즘이 무엇인지 직접 알지 못하며, 오직 전략 인터페이스만을 참조한다. 외부에서 필요에 따라 동적으로 전략을 주입(Dependency Injection, 의존성 주입)받아 실행한다.
예제
전략 패턴을 사용하여 다양한 정렬 알고리즘을 구현해보자.
전략 인터페이스 정의
1
2
3
4
5
6
class SortStrategy
{
public:
virtual void Sort(std::vector<int>& data) = 0;
virtual ~SortStrategy() = default;
};
구체적인 전략 클래스 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class BubbleSort : public SortStrategy
{
public:
void Sort(std::vector<int>& data) override
{ /* 버블 정렬 알고리즘 구현 */ }
};
class QuickSort : public SortStrategy
{
public:
void Sort(std::vector<int>& data) override
{ /* 퀵 정렬 알고리즘 구현 */ }
};
class SelectionSort : public SortStrategy
{
public:
void Sort(std::vector<int>& data) override
{ /* 선택 정렬 알고리즘 구현 */ }
};
문맥 클래스 구현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SortContext
{
private:
SortStrategy* strategy;
public:
SortContext(SortStrategy* strategy) : strategy(strategy) {}
void SetStrategy(SortStrategy* newStrategy)
{
strategy = newStrategy;
}
void Sort(std::vector<int>& data)
{
strategy->Sort(data);
}
};
전략 패턴 사용 예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main()
{
std::vector<int> data = {5, 2, 9, 1, 5, 6};
BubbleSort bubbleSort;
QuickSort quickSort;
SelectionSort selectionSort;
SortContext context(&bubbleSort);
context.Sort(data); // 버블 정렬 사용
// ... 대충 data가 다시 비정렬되었다고 가정
context.SetStrategy(&quickSort);
context.Sort(data); // 퀵 정렬 사용
// ... 대충 data가 다시 비정렬되었다고 가정
context.SetStrategy(&selectionSort);
context.Sort(data); // 선택 정렬 사용
return 0;
}
vs 상태 패턴
전략 패턴과 상태 패턴은 되게 비슷해서 헷갈리기 쉽다.
전략 패턴은 어떻게(how) 할 것인가를 변경하여 알고리즘을 바꾸는 패턴이라면, 상태 패턴은 상태를 변경하여 객체의 행동을 바꾸는 패턴이다.
뭔가 들으니까 더 헷갈리는 것 같다.
전략 패턴은 클라이언트가 필요에 따라 동적으로 알고리즘을 수동으로 교체하고, 상태 패턴은 객체 내부 상태가 변함에 따라 행동이 자동으로 바뀐다.
또한 전략 패턴은 전략 클래스들이 서로의 존재를 모르지만, 상태 패턴에서 상태 클래스들은 다음 상태로의 전이(transition)를 알고 있는 경우가 많다. 상태 패턴 구현하다보면 OnEnter, OnExit 같은 함수를 만드는데 이 함수에서 다음/이전 상태에 대한 정보가 있는 경우가 대부분이다.
참고
- https://cppdeveloper.tistory.com/entry/C-%EA%B3%A0%EA%B8%89-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D%EA%B3%BC-%EC%9D%91%EC%9A%A9-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%8B%9C%EB%A6%AC%EC%A6%88-Day-10-%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EC%8B%AC%ED%99%94-%EC%A0%84%EB%9E%B5-%ED%8C%A8%ED%84%B4-Strategy-Pattern
- https://victorydntmd.tistory.com/292