Strategy Pattern
알고리즘을 객체로 캡슐화해 if/else 분기 없이 런타임에 갈아끼우는 패턴. Context는 Strategy 인터페이스에만 의존하고 ConcreteStrategy를 모른다.
난이도 입문 · 선행 없음
한 줄 요약
알고리즘 후보를 각각 클래스로 분리하고, 사용하는 쪽(Context)은 인터페이스 뒤에 있는 어느 구현체를 쓰는지 모른 채 위임한다. if (type == "card") ... else if (type == "kakao") ... 형태의 분기문이 보이면 Strategy를 떠올리면 된다.
어떤 문제를 푸는가
결제 처리기에서 결제 수단별 분기를 떠올려보자.
1
2
3
4
5
6
7
8
9
10
11
12
class PaymentProcessor {
public:
void pay(const std::string& method, int amount) {
if (method == "card") {
// 카드 결제 로직 (30줄)
} else if (method == "kakao") {
// 카카오페이 로직 (20줄)
} else if (method == "toss") {
// 토스 로직 (25줄)
}
}
};
이 코드의 통증:
- 결제 수단이 늘어날 때마다
PaymentProcessor를 수정해야 한다 (OCP 위반). - 한 클래스가 무관한 로직 묶음을 동시에 들고 있어 변경 사유가 여러 개다 (SRP 위반).
- 카카오페이 하나를 고치려고 카드·토스 코드까지 같이 읽어야 한다.
- 결제 수단별로 독립 테스트가 어렵다.
패턴 적용 후
분기 안에 있던 알고리즘 하나하나를 별도 클래스로 떼어내고, 공통 인터페이스로 묶는다.
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
class PaymentStrategy {
public:
virtual void pay(int amount) = 0;
virtual ~PaymentStrategy() = default;
};
class CardPayment : public PaymentStrategy {
void pay(int amount) override {
std::cout << "카드로 " << amount << "원 결제" << std::endl;
}
};
class KakaoPayment : public PaymentStrategy {
void pay(int amount) override {
std::cout << "카카오페이로 " << amount << "원 결제" << std::endl;
}
};
class PaymentProcessor {
std::unique_ptr<PaymentStrategy> strategy;
public:
void setStrategy(std::unique_ptr<PaymentStrategy> s) {
strategy = std::move(s);
}
void process(int amount) {
strategy->pay(amount);
}
};
int main() {
PaymentProcessor processor;
processor.setStrategy(std::make_unique<CardPayment>());
processor.process(10000);
processor.setStrategy(std::make_unique<KakaoPayment>());
processor.process(5000);
}
달라진 점:
- 결제 수단 추가는 새 클래스를 하나 만드는 일이다.
PaymentProcessor는 손대지 않는다. - 각 결제 수단을 단독으로 테스트할 수 있다.
- 런타임에 전략을 교체할 수 있다.
구조
- Strategy: 알고리즘들의 공통 인터페이스
- ConcreteStrategy: 실제 알고리즘 구현 (
CardPayment,KakaoPayment) - Context: 전략을 보유하고 위임하는 객체 (
PaymentProcessor)
1
2
3
4
5
Context ──has──▶ Strategy (interface)
▲
│
┌──────────────┼──────────────┐
ConcreteStrategyA ConcreteStrategyB ConcreteStrategyC
핵심은 Context가 ConcreteStrategy를 직접 알지 못한다는 점이다. 인터페이스에만 의존한다.
함수형 변형 — std::function / 람다
전략에 상태가 없고 메서드가 하나뿐이면 클래스를 만들 필요 없이 함수 객체로 충분하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <functional>
class PaymentProcessor {
std::function<void(int)> payFn;
public:
void setPayment(std::function<void(int)> fn) { payFn = std::move(fn); }
void process(int amount) { payFn(amount); }
};
int main() {
PaymentProcessor processor;
processor.setPayment([](int amount) {
std::cout << "카드로 " << amount << "원 결제" << std::endl;
});
processor.process(10000);
processor.setPayment([](int amount) {
std::cout << "카카오페이로 " << amount << "원 결제" << std::endl;
});
processor.process(5000);
}
- 전략이 단순 함수 한 줄이면 람다.
- 전략에 상태(설정값, 의존성)나 여러 메서드가 묶이면 클래스.
실전 사례
std::sort(begin, end, comparator): 세 번째 인자(비교 함수)가 정렬 전략이다.std::less,std::greater, 직접 작성한 람다 모두 같은 자리를 채운다.- Spring
PasswordEncoder:BCryptPasswordEncoder,Argon2PasswordEncoder같은 구현체를 빈으로 갈아끼우면 인증 코드 본체는 그대로다. - 결제 PG 어댑터: 토스/카카오/네이버페이를 같은 인터페이스 뒤로 통일하는 구조가 위 예제와 정확히 같다.
State Pattern과의 차이
구조가 거의 같아 헷갈리기 쉽다. 의도가 다르다.
| Strategy | State | |
|---|---|---|
| 무엇을 바꾸나 | 알고리즘 | 상태 |
| 핵심 의도 | 교체 가능한 정책·알고리즘 | lifecycle 상태에 따른 행동 |
| 선택·전이 주체 | 클라이언트·설정·Context 모두 가능 | State·Context·전이표 모두 가능 |
| 전이 개념 | 없음 — 독립적인 선택지들 | 있음 — A → B → C 흐름 |
| 사용 예 | 결제 수단, 정렬 비교자, 압축 알고리즘 | 신호등, 주문 진행, TCP 연결 |
Strategy는 어떤 정책을 쓸지가 핵심이고, State는 현재 lifecycle 상태가 무엇인지가 핵심입니다. 구체 State가 다음 State를 아는 구현은 흔하지만 필수는 아닙니다.
자세한 비교는 State 패턴 글 참고.
안티패턴 / 주의
- 전략이 하나뿐인데 미리 도입하지 말 것. 두 번째 알고리즘이 실제로 등장할 때 추출해도 늦지 않다.
- Context가 ConcreteStrategy의 디테일을 알아채서 분기하면 캡슐화가 깨진다. 그럴 거면 그냥 if/else가 낫다.
- 전략 간 데이터 공유가 필요하면 Strategy 인터페이스 인자로 명시적으로 넘긴다. Context 내부 상태에 손을 뻗는 전략은 결합이 풀린 게 아니다.
스스로 점검
1. 카드/카카오/토스에 네이버페이를 추가하려면 위 코드에서 어디를 수정해야 할까?
답
NaverPayment 클래스 하나만 추가하면 된다. PaymentProcessor도, 기존 결제 클래스(CardPayment, KakaoPayment)도 손대지 않는다. 이게 OCP(개방-폐쇄 원칙)를 만족한다는 뜻.
2. 지금은 결제 수단이 카드 하나뿐이다. Strategy 패턴을 미리 도입해두는 게 좋을까?
답
아니다. 두 번째 알고리즘이 실제로 등장할 때 추출해도 늦지 않다 (YAGNI). 전략이 하나뿐이면 추상화 비용만 늘어나고 가독성을 해친다.
3. Strategy와 State는 구조가 거의 같다. 둘을 구별하는 가장 명확한 단서는?
답
의도를 봅니다. Strategy는 같은 목적의 정책·알고리즘을 교체하고, State는 객체의 lifecycle 상태에 따라 행동과 허용 전이를 표현합니다. 누가 전략을 선택하고 누가 상태를 전이시키는지는 구현 선택이라 정의 기준이 아닙니다.