소프트웨어 디자인 패턴

전략 패턴 쉽게 이해하기

Enchantée 2026. 6. 20. 16:09
728x90
반응형

디자인 패턴을 공부하다 보면 가장 먼저 자주 만나는 패턴 중 하나가 전략 패턴입니다.

처음에는 이름이 조금 거창해 보이지만, 핵심은 단순합니다.

바뀔 수 있는 알고리즘이나 정책을 별도의 객체로 분리하고, 실행 시점에 갈아 끼울 수 있게 만드는 방식입니다.

 

전략 패턴은 조건문으로 늘어나는 정책 코드를 객체로 분리해, 변경에는 열려 있고 기존 코드 수정에는 닫힌 구조를 만드는 패턴입니다.

 

이번 글에서는 할인 정책 예제를 통해 전략 패턴이 왜 필요한지, C++에서는 어떻게 구현할 수 있는지, 그리고 실무에서는 언제 쓰고 언제 피해야 하는지 정리해보겠습니다.

 


1. 전략 패턴은 무엇인가?

전략 패턴은 Strategy Pattern이라고 부릅니다.

GoF 디자인 패턴 중 하나이며, 같은 목적을 가진 여러 알고리즘을 캡슐화하고 필요에 따라 교체할 수 있게 만드는 패턴입니다.

여기서 알고리즘이라는 말은 꼭 정렬 알고리즘처럼 거창한 것만 의미하지 않습니다.

실무에서는 할인 정책, 결제 수수료 계산, 배송비 계산, 정렬 기준, 인증 방식, 압축 방식, 재시도 정책 같은 것도 전략이 될 수 있습니다.

구성 요소 역할
Context 전략을 사용하는 객체
Strategy 공통 인터페이스
ConcreteStrategy 실제 알고리즘 또는 정책 구현
Client 어떤 전략을 사용할지 결정하는 코드

전략 패턴의 핵심은 Context가 구체적인 전략 클래스를 직접 알지 않게 만드는 것입니다.

Context는 공통 인터페이스만 알고, 실제 동작은 주입된 전략 객체에게 위임합니다.

 


2. 왜 전략 패턴이 필요한가?

할인 정책을 예로 들어보겠습니다.

처음에는 할인 정책이 하나뿐이라 간단한 조건문으로 충분할 수 있습니다.

int calculateDiscount(const std::string& type, int price) {
    if (type == "none") {
        return 0;
    }

    if (type == "fixed") {
        return 3000;
    }

    if (type == "rate") {
        return price / 10;
    }

    return 0;
}

 

이 정도 코드는 크게 문제가 없어 보입니다.

하지만 실무에서는 정책이 계속 늘어납니다.

  1. 첫 구매 할인
  2. VIP 할인
  3. 쿠폰 할인
  4. 기간 한정 할인
  5. 카테고리별 할인
  6. 국가별 세금 또는 배송비 정책

정책이 추가될 때마다 if 또는 switch 문이 계속 커지고, 기존 함수를 계속 수정해야 합니다.

이런 구조는 테스트하기도 어렵고, 한 정책을 고치다가 다른 정책에 영향을 줄 가능성도 커집니다.

전략 패턴은 이 문제를 각 정책 클래스로 분리해서 해결합니다.

 


3. 구조를 그림으로 보기

전략 패턴의 구조를 단순화하면 다음처럼 볼 수 있습니다.

- Strategy Pattern 구조

Client
   |
   v
Context -------------------> Strategy interface
   |                              ^
   |                              |
   |                  +-----------+------------+
   |                  |           |            |
   v                  v           v            v
실제 작업 수행     NoDiscount  FixedDiscount  RateDiscount

 

Client는 상황에 맞는 전략을 선택해서 Context에 전달합니다.

Context는 구체적인 할인 정책을 직접 판단하지 않고 Strategy 인터페이스를 통해 계산을 위임합니다.

이렇게 하면 새로운 정책이 추가되어도 Context 코드를 크게 바꾸지 않아도 됩니다.

 


4. C++로 전략 패턴 구현하기

이제 할인 정책을 전략 패턴으로 구현해보겠습니다.

C++에서는 보통 추상 클래스와 가상 함수를 이용해 전략 인터페이스를 만들 수 있습니다.

#include <iostream>
#include <memory>
#include <string>
#include <utility>

struct Order {
    int price = 0;
    bool firstPurchase = false;
};

class DiscountStrategy {
public:
    virtual ~DiscountStrategy() = default;
    virtual int discount(const Order& order) const = 0;
    virtual std::string name() const = 0;
};

class NoDiscount : public DiscountStrategy {
public:
    int discount(const Order&) const override {
        return 0;
    }

    std::string name() const override {
        return "no discount";
    }
};

class FixedDiscount : public DiscountStrategy {
public:
    explicit FixedDiscount(int amount) : amount_(amount) {}

    int discount(const Order&) const override {
        return amount_;
    }

    std::string name() const override {
        return "fixed discount";
    }

private:
    int amount_;
};

class RateDiscount : public DiscountStrategy {
public:
    explicit RateDiscount(int percent) : percent_(percent) {}

    int discount(const Order& order) const override {
        return order.price * percent_ / 100;
    }

    std::string name() const override {
        return "rate discount";
    }

private:
    int percent_;
};

class PriceCalculator {
public:
    explicit PriceCalculator(std::unique_ptr<DiscountStrategy> strategy)
        : strategy_(std::move(strategy)) {}

    void setStrategy(std::unique_ptr<DiscountStrategy> strategy) {
        strategy_ = std::move(strategy);
    }

    int finalPrice(const Order& order) const {
        const int discounted = order.price - strategy_->discount(order);
        return discounted < 0 ? 0 : discounted;
    }

    std::string strategyName() const {
        return strategy_->name();
    }

private:
    std::unique_ptr<DiscountStrategy> strategy_;
};

int main() {
    Order order{10000, true};

    PriceCalculator calculator(std::make_unique<NoDiscount>());
    std::cout << calculator.strategyName() << ": "
              << calculator.finalPrice(order) << '\n';

    calculator.setStrategy(std::make_unique<RateDiscount>(10));
    std::cout << calculator.strategyName() << ": "
              << calculator.finalPrice(order) << '\n';

    calculator.setStrategy(std::make_unique<FixedDiscount>(3000));
    std::cout << calculator.strategyName() << ": "
              << calculator.finalPrice(order) << '\n';
}

 

PriceCalculator는 구체적인 할인 정책을 직접 알지 않습니다.

NoDiscount, RateDiscount, FixedDiscount 중 무엇이 들어와도 DiscountStrategy 인터페이스만 사용합니다.

새로운 할인 정책을 추가하고 싶다면 DiscountStrategy를 상속한 클래스를 새로 만들면 됩니다.

기존 PriceCalculator의 핵심 로직을 수정하지 않아도 된다는 점이 전략 패턴의 장점입니다.

 


5. 전략 패턴과 Open-Closed Principle

전략 패턴은 객체지향 설계 원칙 중 Open-Closed Principle과 자주 함께 설명됩니다.

Open-Closed Principle은 다음과 같은 원칙입니다.

소프트웨어 요소는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.

 

할인 정책이 계속 늘어나는 상황을 생각해보겠습니다.

if 문 중심 구조에서는 새로운 할인 정책을 추가할 때마다 기존 calculateDiscount 함수를 수정해야 합니다.

반면 전략 패턴에서는 새로운 ConcreteStrategy를 추가하면 됩니다.

즉, 기능은 확장하지만 기존 Context 코드는 크게 건드리지 않습니다.

이 차이는 정책이 많아질수록 커집니다.

구분 조건문 중심 전략 패턴
정책 추가 기존 조건문 수정 새 전략 클래스 추가
테스트 큰 함수 전체를 함께 테스트해야 함 전략별 단위 테스트가 쉬움
변경 영향 기존 분기와 충돌 가능 변경 범위를 좁히기 쉬움
구조 복잡도 초기에는 단순함 클래스와 인터페이스가 늘어남

다만 전략 패턴이 항상 더 좋은 것은 아닙니다.

정책이 2개뿐이고 앞으로 늘어날 가능성이 낮다면 간단한 조건문이 더 읽기 쉬울 수 있습니다.

 


6. std::function으로도 전략을 표현할 수 있다

C++에서 전략 패턴을 반드시 상속과 가상 함수로만 구현해야 하는 것은 아닙니다.

간단한 정책이라면 std::function과 람다를 이용해 전략을 표현할 수도 있습니다.

#include <functional>
#include <iostream>
#include <string>

struct Order {
    int price = 0;
    bool firstPurchase = false;
};

using DiscountPolicy = std::function<int(const Order&)>;

class PriceCalculator {
public:
    explicit PriceCalculator(DiscountPolicy policy)
        : policy_(std::move(policy)) {}

    int finalPrice(const Order& order) const {
        const int discounted = order.price - policy_(order);
        return discounted < 0 ? 0 : discounted;
    }

private:
    DiscountPolicy policy_;
};

int main() {
    Order order{10000, true};

    PriceCalculator firstPurchaseCalculator([](const Order& order) {
        return order.firstPurchase ? 2000 : 0;
    });

    PriceCalculator rateCalculator([](const Order& order) {
        return order.price * 15 / 100;
    });

    std::cout << firstPurchaseCalculator.finalPrice(order) << '\n';
    std::cout << rateCalculator.finalPrice(order) << '\n';
}

 

이 방식은 클래스 수를 줄일 수 있어 간단한 정책에는 편합니다.

하지만 정책에 이름, 상태, 여러 메서드, 복잡한 테스트가 필요하다면 인터페이스 기반 전략이 더 명확할 수 있습니다.

즉, 중요한 것은 패턴의 형태를 외우는 것이 아니라 바뀌는 정책을 분리한다는 의도입니다.

 


7. 언제 전략 패턴을 쓰면 좋을까?

전략 패턴은 다음 상황에서 특히 유용합니다.

  1. 비슷한 목적의 알고리즘이나 정책이 여러 개 있다.
  2. 조건문이 계속 늘어나고 있다.
  3. 정책을 실행 시점에 바꿔야 한다.
  4. 정책별로 테스트를 분리하고 싶다.
  5. Context는 정책의 세부 구현을 몰라도 된다.

예를 들어 결제 수단별 수수료 계산, 배송사별 배송비 계산, 검색 결과 정렬 기준, 캐시 교체 정책, 파일 압축 방식 선택 같은 곳에 적용할 수 있습니다.

정책이 자주 바뀌거나 실험 대상이라면 전략 패턴이 더 유용해집니다.

반대로 정책이 거의 변하지 않고 분기가 1~2개뿐이라면 굳이 패턴을 적용하지 않는 편이 좋을 수 있습니다.

 


8. 자주 하는 오해

8-1. 전략 패턴은 if 문을 무조건 없애는 패턴이다

전략 패턴은 모든 조건문을 없애기 위한 패턴이 아닙니다.

어떤 전략을 선택할지 결정하는 지점에는 여전히 조건문이나 설정값이 필요할 수 있습니다.

핵심은 조건문을 완전히 제거하는 것이 아니라, 바뀌는 정책의 구현을 Context에서 분리하는 것입니다.


8-2. 전략 패턴은 상속을 반드시 써야 한다

전통적인 설명에서는 인터페이스와 구현 클래스를 사용하지만, C++에서는 std::function, 템플릿, 함수 객체로도 전략을 표현할 수 있습니다.

상속은 선택지 중 하나입니다.

정책이 단순하면 람다 기반 전략이 더 읽기 쉬울 수 있고, 정책이 복잡하면 클래스 기반 전략이 더 관리하기 쉬울 수 있습니다.


8-3. 디자인 패턴을 적용하면 무조건 좋은 설계가 된다

패턴은 문제를 해결하기 위한 도구일 뿐입니다.

변화 가능성이 낮은 코드에 전략 패턴을 적용하면 클래스만 늘어나고 읽기 어려운 코드가 될 수 있습니다.

먼저 변경 방향을 확인하고, 실제로 분리할 가치가 있을 때 적용하는 것이 좋습니다.

 


9. 실무에서는 어떻게 볼까?

실무에서 전략 패턴을 볼 때는 클래스 다이어그램보다 변경 이유를 먼저 봐야 합니다.

다음 질문에 답할 수 있으면 전략 패턴을 적용할 만한 상황인지 판단하기 쉽습니다.

  1. 현재 코드에서 자주 바뀌는 부분은 어디인가?
  2. 그 변경 때문에 기존 코드가 반복해서 수정되는가?
  3. 정책별 테스트를 분리하면 이득이 있는가?
  4. 실행 중 정책을 바꿔야 하는가?
  5. 정책이 늘어날 가능성이 높은가?

전략 패턴을 잘 적용하면 핵심 흐름과 정책 구현이 분리됩니다.

예를 들어 주문 처리 흐름은 PriceCalculator나 OrderService가 담당하고, 할인 계산만 DiscountStrategy가 담당하게 만들 수 있습니다.

이렇게 역할이 분리되면 기능 추가와 테스트가 쉬워집니다.

다만 전략 객체 생성 위치까지 무분별하게 흩어지면 오히려 추적이 어려워질 수 있습니다.

그래서 실제 프로젝트에서는 설정, factory, dependency injection 같은 방식으로 전략 선택 지점을 한곳에 모으는 경우가 많습니다.

 


10. 면접 질문 예시

질문 1. 전략 패턴은 어떤 문제를 해결하나요?

전략 패턴은 같은 목적을 가진 여러 알고리즘이나 정책이 조건문으로 섞이는 문제를 해결합니다.

바뀌는 부분을 Strategy 인터페이스 뒤로 분리하고, Context는 공통 인터페이스에만 의존하게 만듭니다.

그 결과 새로운 정책을 추가할 때 기존 Context 코드를 크게 수정하지 않아도 됩니다.


질문 2. 전략 패턴과 상태 패턴은 어떻게 다른가요?

둘 다 객체를 교체하면서 동작을 바꿀 수 있다는 점은 비슷합니다.

하지만 전략 패턴은 보통 클라이언트가 어떤 알고리즘을 사용할지 선택하는 구조입니다.

반면 상태 패턴은 객체 내부 상태 변화에 따라 동작이 바뀌는 구조에 가깝습니다.


질문 3. 전략 패턴의 단점은 무엇인가요?

전략 패턴을 적용하면 클래스나 객체 수가 늘어날 수 있습니다.

또한 어떤 전략이 선택되는지 추적하기 어려워질 수 있습니다.

따라서 정책이 정말로 자주 바뀌거나 늘어나는지 확인하고, 전략 선택 지점을 명확하게 관리하는 것이 중요합니다.

 


11. 정리

전략 패턴은 바뀌는 알고리즘이나 정책을 별도 객체로 분리하는 디자인 패턴입니다.

핵심만 정리하면 다음과 같습니다.

  • 전략 패턴은 Context와 Strategy를 분리한다.
  • Context는 구체적인 정책이 아니라 공통 인터페이스에 의존한다.
  • 새 정책을 추가할 때 기존 Context 수정을 줄일 수 있다.
  • C++에서는 추상 클래스, std::function, 함수 객체 등으로 구현할 수 있다.
  • 조건문이 커지고 정책 변경이 잦을 때 유용하다.
  • 변화 가능성이 낮은 단순 분기에는 오히려 과한 설계가 될 수 있다.

디자인 패턴을 공부할 때 중요한 것은 패턴 이름을 외우는 것이 아닙니다.

어떤 변경을 어디에서 흡수하려는 설계인지 이해하는 것입니다.

전략 패턴은 그 관점을 익히기에 좋은 첫 번째 패턴입니다.

 


728x90
반응형