포스트

Strategy Pattern

알고리즘을 객체로 캡슐화해 if/else 분기 없이 런타임에 갈아끼우는 패턴. Context는 Strategy 인터페이스에만 의존하고 ConcreteStrategy를 모른다.

Strategy Pattern

난이도 입문 · 선행 없음

한 줄 요약

알고리즘 후보를 각각 클래스로 분리하고, 사용하는 쪽(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과의 차이

구조가 거의 같아 헷갈리기 쉽다. 의도가 다르다.

 StrategyState
무엇을 바꾸나알고리즘상태
핵심 의도교체 가능한 정책·알고리즘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 상태에 따라 행동과 허용 전이를 표현합니다. 누가 전략을 선택하고 누가 상태를 전이시키는지는 구현 선택이라 정의 기준이 아닙니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.