무엇인가

알고리즘을 갈아 끼울 수 있게 밖으로 빼낸다. 쓰는 쪽 코드는 그대로 두고 방법만 바꾼다.

GoF 패턴 중 가장 순수하게 “메서드 하나짜리 인터페이스” 다. 그래서 가장 깔끔하게 접힌다 — 사실상 std::function 그 자체다.

구조

graph LR
    CTX[문맥: 정렬하는 쪽] -->|비교 방법을 물어본다| S[전략]
    S --> S1[오름차순]
    S --> S2[내림차순]
    S --> S3[길이순]

클래스판

#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
 
struct CompareStrategy {
    virtual ~CompareStrategy() = default;
    virtual bool less(const std::string& a, const std::string& b) const = 0;
};
 
struct Ascending : CompareStrategy {
    bool less(const std::string& a, const std::string& b) const override { return a < b; }
};
struct Descending : CompareStrategy {
    bool less(const std::string& a, const std::string& b) const override { return b < a; }
};
struct ByLength : CompareStrategy {
    bool less(const std::string& a, const std::string& b) const override {
        return a.size() < b.size();
    }
};
 
class Sorter {
    const CompareStrategy& strategy_;
 
  public:
    explicit Sorter(const CompareStrategy& s) : strategy_(s) {}
    void sort(std::vector<std::string>& v) const {
        std::sort(v.begin(), v.end(), [this](const std::string& a, const std::string& b) {
            return strategy_.less(a, b);
        });
    }
};
 
void print(const std::vector<std::string>& v) {
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}
 
int main() {
    std::vector<std::string> v{"banana", "fig", "apple", "cherry"};
 
    Ascending asc;
    Descending desc;
    ByLength len;
 
    auto a = v; Sorter{asc}.sort(a);  print(a);
    auto b = v; Sorter{desc}.sort(b); print(b);
    auto c = v; Sorter{len}.sort(c);  print(c);
}

클래스가 다섯이다(인터페이스 1 + 전략 3 + 문맥 1). 그리고 Sorter::sort 안에서 이미 람다를 쓰고 있다 — 표준 라이브러리가 원래 이 방식이기 때문이다. 전략 인터페이스는 그 람다를 한 겹 더 감싼 것에 지나지 않는다.

람다판

less 하나뿐인 인터페이스다. 통째로 사라진다.

#include <algorithm>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
 
using Compare = std::function<bool(const std::string&, const std::string&)>;
 
void sortWith(std::vector<std::string>& v, const Compare& cmp) {
    std::sort(v.begin(), v.end(), cmp);
}
 
void print(const std::vector<std::string>& v) {
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}
 
int main() {
    std::vector<std::string> v{"banana", "fig", "apple", "cherry"};
 
    auto a = v;
    sortWith(a, [](const std::string& x, const std::string& y) { return x < y; });
    print(a);
 
    auto b = v;
    sortWith(b, [](const std::string& x, const std::string& y) { return y < x; });
    print(b);
 
    auto c = v;
    sortWith(c, [](const std::string& x, const std::string& y) { return x.size() < y.size(); });
    print(c);
 
    // 전략을 즉석에서 만든다 — 클래스판이라면 새 클래스가 필요했다
    const char target = 'a';
    auto count = [target](const std::string& s) {
        return std::count(s.begin(), s.end(), target);
    };
    auto d = v;
    sortWith(d, [count](const std::string& x, const std::string& y) {
        return count(x) > count(y);
    });
    print(d);
}

클래스가 0이다. 그리고 마지막 정렬이 요점이다 — “특정 글자가 많은 순”이라는 전략을 그 자리에서 만들었다. 클래스판이라면 ByCharCount 클래스를 정의하고, target을 받을 생성자를 붙이고, 인스턴스를 만들어 넘겨야 했다.

무엇이 달라졌나

클래스판람다판
전략 3개일 때 클래스 수50
전략에 파라미터멤버 + 생성자캡처
일회용 전략클래스를 만들어야 한다그 자리에서
전략의 이름타입 이름으로 남는다남지 않는다

마지막 줄이 유일하게 클래스판이 이기는 항목이다. Descending이라는 이름은 그 자체로 문서지만 [](auto& x, auto& y){ return y < x; }는 아니다.

뜨거운 경로라면 템플릿으로

std::function은 간접 호출이고 경우에 따라 힙을 쓴다. 정렬 비교처럼 수없이 불리는 자리라면 타입을 지우지 않는 편이 낫다.

template <typename Compare>
void sortWith(std::vector<std::string>& v, Compare cmp) {
    std::sort(v.begin(), v.end(), cmp);
}

std::sort가 원래 이렇게 생겼다. 비교자를 템플릿 매개변수로 받으므로 람다가 인라인될 여지가 있다. std::function으로 감싸면 그 여지가 사라진다.

전략을 저장해 둬야 할 때만 std::function을 쓰고, 호출 지점에서 바로 쓸 때는 템플릿으로 두는 것이 기본이다.

언제 접고 언제 접지 않나

접는다 — 거의 항상. 이 패턴은 C++에서 사실상 람다로 대체됐다.

접지 않는다

  • 전략이 여러 메서드를 묶어야 할 때(예: encode/decode 쌍).
  • 전략에 이름이 필요할 때. 설정 파일이나 UI에서 골라야 한다면 std::map<std::string, Compare>처럼 이름과 람다를 따로 묶는 방법이 있다.
  • 전략이 무거운 상태를 들고 여러 곳에서 공유돼야 할 때.