C++ Strategy Pattern 완벽 가이드 | 알고리즘 캡슐화와 런타임 교체

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. 기본 구조 (다형성)
  2. 함수 포인터 방식
  3. 람다 방식
  4. std::function 방식
  5. 자주 발생하는 문제와 해결법
  6. 프로덕션 패턴
  7. 완전한 예제: 압축 알고리즘
  8. 성능 비교

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:

한 줄 요약: Strategy Pattern으로 알고리즘을 캡슐화하고 런타임에 교체할 수 있습니다. 다음으로 Command Pattern을 읽어보면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ 가상 함수 | “Virtual Functions” 가이드
  • C++ Observer Pattern 완벽 가이드 | 이벤트 기반 아키텍처와 신호/슬롯

관련 글

  • C++ CRTP 완벽 가이드 | 정적 다형성과 컴파일 타임 최적화
  • C++ Factory Pattern 완벽 가이드 | 객체 생성 캡슐화와 확장성
  • C++ Visitor Pattern |
  • 배열과 리스트 | 코딩 테스트 필수 자료구조 완벽 정리
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성