C++ Strategy Pattern 완벽 가이드 | 알고리즘 캡슐화와 런타임 교체
이 글의 핵심
C++ Strategy Pattern 완벽 가이드에 대한 실전 가이드입니다. 알고리즘 캡슐화와 런타임 교체 등을 예제와 함께 상세히 설명합니다.
Strategy Pattern이란? 왜 필요한가
문제 시나리오: 알고리즘 하드코딩
문제: 정렬 알고리즘을 선택하는 로직이 Context에 하드코딩되면, 새 알고리즘 추가 시 Context를 수정해야 합니다.
// 나쁜 예: 알고리즘 하드코딩
class Sorter {
public:
void sort(std::vector<int>& data, const std::string& algorithm) {
if (algorithm == "bubble") {
// 버블 정렬
} else if (algorithm == "quick") {
// 퀵 정렬
} else if (algorithm == "merge") {
// 병합 정렬
}
// 새 알고리즘 추가 시 여기 수정
}
};
해결: Strategy Pattern은 알고리즘을 캡슐화해 런타임에 교체 가능하게 합니다.
행동 패턴 시리즈·State 패턴과 “알고리즘 교체 vs 상태 전이”를 비교해 읽으면 좋습니다.
// 좋은 예: Strategy Pattern
class SortStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
virtual ~SortStrategy() = default;
};
class BubbleSort : public SortStrategy {
void sort(std::vector<int>& data) override { /* ... */ }
};
class Sorter {
public:
void setStrategy(std::unique_ptr<SortStrategy> s) {
strategy = std::move(s);
}
void sort(std::vector<int>& data) {
strategy->sort(data);
}
private:
std::unique_ptr<SortStrategy> strategy;
};
flowchart TD
context["Context (Sorter)"]
strategy["Strategy (SortStrategy)"]
bubble["BubbleSort"]
quick["QuickSort"]
merge["MergeSort"]
context --> strategy
strategy <|-- bubble
strategy <|-- quick
strategy <|-- merge
목차
1. 기본 구조 (다형성)
최소 Strategy
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
class SortStrategy {
public:
virtual void sort(std::vector<int>& data) = 0;
virtual std::string name() const = 0;
virtual ~SortStrategy() = default;
};
class BubbleSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - i - 1; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
std::string name() const override { return "BubbleSort"; }
};
class QuickSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::sort(data.begin(), data.end());
}
std::string name() const override { return "QuickSort"; }
};
class Sorter {
public:
void setStrategy(std::unique_ptr<SortStrategy> s) {
strategy = std::move(s);
}
void sort(std::vector<int>& data) {
if (strategy) {
std::cout << "Using " << strategy->name() << '\n';
strategy->sort(data);
}
}
private:
std::unique_ptr<SortStrategy> strategy;
};
int main() {
Sorter sorter;
std::vector<int> data = {5, 2, 8, 1, 9};
sorter.setStrategy(std::make_unique<BubbleSort>());
sorter.sort(data); // Using BubbleSort
sorter.setStrategy(std::make_unique<QuickSort>());
sorter.sort(data); // Using QuickSort
}
2. 함수 포인터 방식
간단한 알고리즘
#include <iostream>
#include <vector>
#include <algorithm>
using SortFunc = void(*)(std::vector<int>&);
void bubbleSort(std::vector<int>& data) {
for (size_t i = 0; i < data.size(); ++i) {
for (size_t j = 0; j < data.size() - i - 1; ++j) {
if (data[j] > data[j + 1]) {
std::swap(data[j], data[j + 1]);
}
}
}
}
void quickSort(std::vector<int>& data) {
std::sort(data.begin(), data.end());
}
class Sorter {
public:
void setStrategy(SortFunc func) {
strategy = func;
}
void sort(std::vector<int>& data) {
if (strategy) {
strategy(data);
}
}
private:
SortFunc strategy = nullptr;
};
int main() {
Sorter sorter;
std::vector<int> data = {5, 2, 8, 1, 9};
sorter.setStrategy(bubbleSort);
sorter.sort(data);
sorter.setStrategy(quickSort);
sorter.sort(data);
}
3. 람다 방식
인라인 알고리즘
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
class Sorter {
public:
using Strategy = std::function<void(std::vector<int>&)>;
void setStrategy(Strategy s) {
strategy = s;
}
void sort(std::vector<int>& data) {
if (strategy) {
strategy(data);
}
}
private:
Strategy strategy;
};
int main() {
Sorter sorter;
std::vector<int> data = {5, 2, 8, 1, 9};
// 람다로 Strategy 정의
sorter.setStrategy( {
std::sort(data.begin(), data.end());
});
sorter.sort(data);
// 역순 정렬
sorter.setStrategy( {
std::sort(data.begin(), data.end(), std::greater<>());
});
sorter.sort(data);
}
4. std::function 방식
유연한 Strategy
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
class PaymentStrategy {
public:
using Strategy = std::function<bool(double)>;
void setStrategy(Strategy s) {
strategy = s;
}
bool pay(double amount) {
if (strategy) {
return strategy(amount);
}
return false;
}
private:
Strategy strategy;
};
int main() {
PaymentStrategy payment;
// 신용카드
payment.setStrategy( {
std::cout << "Paying $" << amount << " with Credit Card\n";
return true;
});
payment.pay(100.0);
// PayPal
payment.setStrategy( {
std::cout << "Paying $" << amount << " with PayPal\n";
return true;
});
payment.pay(50.0);
}
5. 자주 발생하는 문제와 해결법
문제 1: Strategy nullptr
증상: 크래시.
원인: Strategy가 설정되지 않았습니다.
// ❌ 잘못된 사용: nullptr 검사 없음
void sort(std::vector<int>& data) {
strategy->sort(data); // Crash: nullptr
}
// ✅ 올바른 사용: nullptr 검사
void sort(std::vector<int>& data) {
if (strategy) {
strategy->sort(data);
} else {
throw std::runtime_error("Strategy not set");
}
}
문제 2: 상태 공유
증상: 예상과 다른 동작.
원인: Strategy가 상태를 가지면 재사용 시 문제가 됩니다.
// ❌ 잘못된 사용: 상태 공유
class CountingSort : public SortStrategy {
int count = 0; // 상태
public:
void sort(std::vector<int>& data) override {
++count; // 재사용 시 누적
}
};
// ✅ 올바른 사용: 상태 없는 Strategy
class CountingSort : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
// 상태 없음, 순수 알고리즘
}
};
6. 프로덕션 패턴
패턴 1: 기본 Strategy
class Sorter {
public:
Sorter() : strategy(std::make_unique<QuickSort>()) {} // 기본값
void setStrategy(std::unique_ptr<SortStrategy> s) {
if (s) {
strategy = std::move(s);
}
}
void sort(std::vector<int>& data) {
strategy->sort(data); // 항상 유효
}
private:
std::unique_ptr<SortStrategy> strategy;
};
패턴 2: Strategy Factory
class StrategyFactory {
public:
static std::unique_ptr<SortStrategy> create(const std::string& type) {
if (type == "bubble") return std::make_unique<BubbleSort>();
if (type == "quick") return std::make_unique<QuickSort>();
return nullptr;
}
};
int main() {
Sorter sorter;
sorter.setStrategy(StrategyFactory::create("quick"));
}
7. 완전한 예제: 압축 알고리즘
#include <iostream>
#include <string>
#include <memory>
#include <vector>
class CompressionStrategy {
public:
virtual std::vector<uint8_t> compress(const std::string& data) = 0;
virtual std::string decompress(const std::vector<uint8_t>& data) = 0;
virtual std::string name() const = 0;
virtual ~CompressionStrategy() = default;
};
class ZipCompression : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::string& data) override {
std::cout << "[ZIP] Compressing " << data.size() << " bytes\n";
std::vector<uint8_t> result(data.begin(), data.end());
return result;
}
std::string decompress(const std::vector<uint8_t>& data) override {
std::cout << "[ZIP] Decompressing " << data.size() << " bytes\n";
return std::string(data.begin(), data.end());
}
std::string name() const override { return "ZIP"; }
};
class GzipCompression : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::string& data) override {
std::cout << "[GZIP] Compressing " << data.size() << " bytes\n";
std::vector<uint8_t> result(data.begin(), data.end());
return result;
}
std::string decompress(const std::vector<uint8_t>& data) override {
std::cout << "[GZIP] Decompressing " << data.size() << " bytes\n";
return std::string(data.begin(), data.end());
}
std::string name() const override { return "GZIP"; }
};
class Compressor {
public:
void setStrategy(std::unique_ptr<CompressionStrategy> s) {
strategy = std::move(s);
}
std::vector<uint8_t> compress(const std::string& data) {
if (!strategy) {
throw std::runtime_error("Compression strategy not set");
}
std::cout << "Using " << strategy->name() << " compression\n";
return strategy->compress(data);
}
std::string decompress(const std::vector<uint8_t>& data) {
if (!strategy) {
throw std::runtime_error("Compression strategy not set");
}
return strategy->decompress(data);
}
private:
std::unique_ptr<CompressionStrategy> strategy;
};
int main() {
Compressor compressor;
std::string data = "Hello, World! This is a test.";
compressor.setStrategy(std::make_unique<ZipCompression>());
auto compressed = compressor.compress(data);
auto decompressed = compressor.decompress(compressed);
std::cout << "Result: " << decompressed << "\n\n";
compressor.setStrategy(std::make_unique<GzipCompression>());
compressed = compressor.compress(data);
decompressed = compressor.decompress(compressed);
std::cout << "Result: " << decompressed << '\n';
}
8. 성능 비교
| 방식 | 장점 | 단점 |
|---|---|---|
| 다형성 | 타입 안전, 확장 가능 | vtable 오버헤드, 힙 할당 |
| 함수 포인터 | 빠름, 간단 | 타입 안전 부족, 상태 없음 |
| 람다 | 인라인, 캡처 가능 | 타입 복잡, 디버깅 어려움 |
| std::function | 유연, 모든 callable | 오버헤드 큼, 힙 할당 |
정리
| 개념 | 설명 |
|---|---|
| Strategy Pattern | 알고리즘을 캡슐화해 런타임 교체 |
| 목적 | 알고리즘 독립성, 확장성 |
| 구조 | Context, Strategy, ConcreteStrategy |
| 장점 | OCP 준수, 조건문 제거, 테스트 용이 |
| 단점 | 클래스 증가, 간접 참조 |
| 사용 사례 | 정렬, 압축, 결제, 라우팅 |
Strategy Pattern은 알고리즘을 동적으로 교체해야 하는 상황에서 강력한 디자인 패턴입니다.
FAQ
Q1: Strategy Pattern은 언제 쓰나요?
A: 여러 알고리즘 중 선택해야 하고, 런타임에 교체가 필요할 때 사용합니다.
Q2: 다형성 vs 람다?
A: 확장성이 중요하면 다형성, 간단한 알고리즘이면 람다를 사용하세요.
Q3: State Pattern과 차이는?
A: Strategy는 알고리즘 교체, State는 상태 전이에 집중합니다.
Q4: 성능 오버헤드는?
A: 다형성은 vtable 조회, std::function은 힙 할당 오버헤드가 있습니다. 함수 포인터가 가장 빠릅니다.
Q5: 기본 Strategy는 어떻게 설정하나요?
A: 생성자에서 기본 Strategy를 설정하세요.
Q6: Strategy Pattern 학습 리소스는?
A:
- “Design Patterns” by Gang of Four
- “Head First Design Patterns” by Freeman & Freeman
- Refactoring Guru: Strategy Pattern
한 줄 요약: Strategy Pattern으로 알고리즘을 캡슐화하고 런타임에 교체할 수 있습니다. 다음으로 Command Pattern을 읽어보면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 가상 함수 | “Virtual Functions” 가이드
- C++ Observer Pattern 완벽 가이드 | 이벤트 기반 아키텍처와 신호/슬롯
관련 글
- C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
- C++ Factory Pattern 완벽 가이드 | 객체 생성 캡슐화와 확장성
- C++ Visitor Pattern |
- 배열과 리스트 | 코딩 테스트 필수 자료구조 완벽 정리
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성