본문으로 건너뛰기
Previous
Next
C++ std::function | 콜백·전략 패턴과 함수 객체

C++ std::function | 콜백·전략 패턴과 함수 객체

C++ std::function | 콜백·전략 패턴과 함수 객체

이 글의 핵심

C++ std::function 함수·람다·함수 객체를 변수에 저장, 콜백 패턴 구현, 전략 패턴(Strategy Pattern), operator() 오버로딩, std::bind 사용법, 성능 오버헤드, 실전 이벤트 시스템 구현까지 상세히 설명합니다.

💡 초보자를 위한 한 줄: std::function<R(Args...)>는 “이 시그니처로 호출 가능한 것”을 한 타입에 담습니다. 핫 루프에는 오버헤드가 있을 수 있어, 성능이 빡세면 템플릿으로 callable을 직접 받는 방식도 대안입니다. 13-1 람다 표현식 다음이 읽기 순서에 맞습니다.

들어가며: 함수를 변수에 저장하고 싶다

”버튼 클릭 시 실행할 함수를 어떻게 저장하죠?”

UI 버튼에 클릭 이벤트 핸들러를 등록하고 싶었습니다. 하지만 함수 포인터는 제한적이었습니다. std::function은 “호출 가능한 것(callable—함수, 람다, operator()를 가진 객체 등)“을 하나의 타입으로 감싸서 저장·전달할 수 있게 해 줍니다. 타입이 달라도 시그니처(함수의 반환 타입과 매개변수 타입 목록)만 맞으면 같은 변수에 넣을 수 있어서, 콜백·전략 패턴·이벤트 핸들러를 구현할 때 실무에서 널리 쓰입니다. 다만 인라인되지 않을 수 있어서, 성능이 중요한 경로에서는 템플릿으로 callable을 그대로 받는 방식도 고려할 만합니다. 문제의 코드:

class Button {
    void (*onClick)();  // ❌ 함수 포인터만 가능
public:
    void setOnClick(void (*callback)()) {
        onClick = callback;
    }
    void click() {
        if (onClick) onClick();
    }
};
// ❌ 람다 캡처 불가
int clickCount = 0;
button.setOnClick([&clickCount]() {  // 에러!
    clickCount++;
});

std::function으로 해결:

// 타입 정의
class Button {
    std::function<void()> onClick;  // ✅ 모든 callable 가능
public:
    void setOnClick(std::function<void()> callback) {
        onClick = callback;
    }
    void click() {
        if (onClick) onClick();
    }
};
// ✅ 람다, 함수, 함수 객체 모두 가능
int clickCount = 0;
button.setOnClick([&clickCount]() {
    clickCount++;
});

이 글을 읽으면:

  • std::function으로 콜백을 저장할 수 있습니다.
  • 함수 객체를 만들고 활용할 수 있습니다.
  • 전략 패턴을 구현할 수 있습니다.
  • 실전에서 유연한 코드를 작성할 수 있습니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

추가 문제 시나리오: 언제 std::function이 필요한가?

시나리오 1: 알고리즘 비교 함수를 런타임에 바꾸고 싶다

상황: 사용자가 “오름차순/내림차순”을 UI에서 선택하면, 그에 맞게 정렬해야 합니다. std::sort는 비교 함수를 템플릿 인자로 받아 컴파일 타임에 고정되는데, 런타임에 선택하려면 std::function에 담아야 합니다.

// ❌ 컴파일 타임에 고정됨
std::sort(vec.begin(), vec.end(), std::less<int>{});
// ✅ 런타임에 전략 교체 가능
std::function<bool(int, int)> comparator = std::less<int>{};
if (userWantsDescending) {
    comparator = std::greater<int>{};
}
std::sort(vec.begin(), vec.end(), comparator);

시나리오 2: 네트워크 요청 완료 시 콜백을 호출하고 싶다

상황: HTTP 요청이 비동기로 완료되면 결과를 처리하는 함수를 호출해야 합니다. 요청을 보낸 시점과 완료 시점이 다르므로, 콜백을 저장해 두었다가 나중에 호출해야 합니다. 함수 포인터는 캡처가 있는 람다를 받을 수 없습니다.

// ❌ 함수 포인터: 캡처 불가
void (*onComplete)(int status);
// ✅ std::function: 람다 캡처 가능
std::function<void(int)> onComplete;
std::string requestId = "req-123";
onComplete = [requestId](int status) {
    std::cout << "Request " << requestId << " completed: " << status << "\n";
};

시나리오 3: 여러 타입의 callable을 하나의 컨테이너에 담고 싶다

상황: 이벤트 리스너 목록에 “일반 함수”, “람다”, “멤버 함수를 bind한 것”을 섞어서 등록하고 싶습니다. 타입이 각각 다르므로 std::vector<void(*)()> 같은 단일 타입 컨테이너에는 넣을 수 없습니다. std::function으로 시그니처를 통일하면 같은 벡터에 담을 수 있습니다.

std::vector<std::function<void()>> listeners;
void freeFunction() { std::cout << "Free\n"; }
listeners.push_back(freeFunction);
listeners.push_back( { std::cout << "Lambda\n"; });
struct Handler {
    void handle() { std::cout << "Member\n"; }
} handler;
listeners.push_back(std::bind(&Handler::handle, &handler));
for (auto& fn : listeners) fn();  // Free, Lambda, Member

Callable 타입 관계도

다음은 mermaid 예제 코드입니다.

flowchart TB
    subgraph callable["Callable (호출 가능한 것)"]
        A[일반 함수]
        B[함수 포인터]
        C[람다 표현식]
        D["함수 객체br/operator()"]
        E["std bind 결과"]
    end
    subgraph storage[저장/전달 수단]
        F["std functionbr/(타입 소거, 유연함)"]
        G["템플릿 Funcbr/(인라인, 빠름)"]
    end
    A --> F
    B --> F
    C --> F
    D --> F
    E --> F
    A --> G
    C --> G
    D --> G
    style F fill:#e1f5e1
    style G fill:#e1e5f5

1. std::function 기초

기본 사용법

std::function<int(int, int)>는 “int 두 개를 받아 int를 반환하는 호출 가능한 것”을 담는 타입입니다. 일반 함수 add의 이름을 대입하면 그 함수를 저장하고, func(3, 5)처럼 호출할 수 있습니다. 함수 포인터와 달리 시그니처만 맞으면 람다, 함수 객체, std::bind 결과 등 어떤 callable이든 같은 변수에 넣을 수 있어서, 콜백·전략 패턴을 구현할 때 유용합니다.

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o func_basic func_basic.cpp && ./func_basic
#include <functional>
#include <iostream>
int add(int a, int b) { return a + b; }
int main() {
    std::function<int(int, int)> func = add;
    int result = func(3, 5);  // 8
    std::cout << result << "\n";
    return 0;
}

실행 결과: 8 이 한 줄 출력됩니다.

람다 저장

람다는 “이름 없는 함수 객체”이므로 타입이 컴파일마다 다릅니다. 그래서 auto로만 받거나, std::function 에 담아야 나중에 교체·저장이 가능합니다. std::function<int(int, int)>에 캡처 없는 람다를 대입하면, func(3, 5)로 일반 함수처럼 호출할 수 있고, 캡처가 있는 람다도 같은 시그니처면 같은 std::function 타입에 넣을 수 있습니다.

std::function<int(int, int)> func =  {
    return a + b;
};
int result = func(3, 5);  // 8

멤버 함수 저장

멤버 함수는 “객체 + 함수”가 함께 있어야 하므로, 첫 번째 인자로 객체(참조)를 넘기거나 std::bind로 객체를 묶어 둡니다. std::function<int(Calculator&, int, int)>는 “Calculator 참조와 int 두 개를 받아 int를 반환”하는 타입이므로, func(calc, 3, 5)처럼 호출합니다. std::bind(&Calculator::add, &calc, _1, _2)는 calc를 고정하고 나머지 두 인자만 받는 callable을 만들어, 나중에 boundFunc(3, 5)만으로 호출할 수 있게 합니다.

class Calculator {
public:
    int add(int a, int b) {
        return a + b;
    }
};
Calculator calc;
// 멤버 함수 포인터
std::function<int(Calculator&, int, int)> func = &Calculator::add;
int result = func(calc, 3, 5);  // 8
// 또는 bind 사용
auto boundFunc = std::bind(&Calculator::add, &calc,
                           std::placeholders::_1,
                           std::placeholders::_2);
result = boundFunc(3, 5);  // 8

함수 객체 저장

함수 객체(functor)operator()를 오버로드한 구조체/클래스로, Adder()처럼 인스턴스를 만들어 std::function에 넣을 수 있습니다. std::function은 내부적으로 호출 가능한 대상을 타입 소거(type erasure)해서 저장하므로, Adder, 람다, 함수 포인터 등 서로 다른 타입이라도 시그니처가 같으면 같은 std::function 변수에 대입하고 나중에 교체할 수 있습니다.

struct Adder {
    int operator()(int a, int b) const {
        return a + b;
    }
};
std::function<int(int, int)> func = Adder();
int result = func(3, 5);  // 8

빈 function 확인

std::function<void()> func;
if (!func) {
    std::cout << "Function is empty\n";
}
func =  { std::cout << "Hello\n"; };
if (func) {
    func();  // Hello
}

std::function 시그니처 패턴

시그니처의미
std::function<void()>인자 없음, 반환 없음
std::function<int(int, int)>int 두 개 받아 int 반환
std::function<void(const std::string&)>문자열 받아 void 반환
std::function<bool(int, int)>비교자 (정렬 등)

2. 함수 객체 (Functor)

기본 함수 객체

struct Multiplier {
    int factor;
    Multiplier(int f) : factor(f) {}
    int operator()(int x) const {
        return x * factor;
    }
};
int main() {
    Multiplier times2(2);
    Multiplier times10(10);
    std::cout << times2(5) << "\n";   // 10
    std::cout << times10(5) << "\n";  // 50
}

상태를 가진 함수 객체

class Counter {
    int count = 0;
public:
    int operator()() {
        return ++count;
    }
    int getCount() const { return count; }
};
int main() {
    Counter counter;
    std::cout << counter() << "\n";  // 1
    std::cout << counter() << "\n";  // 2
    std::cout << counter() << "\n";  // 3
    std::cout << "Total: " << counter.getCount() << "\n";  // 3
}

STL과 함께 사용

struct IsEven {
    bool operator()(int x) const {
        return x % 2 == 0;
    }
};
int main() {
    std::vector<int> numbers = {1, 2, 3, 4, 5, 6};
    // 짝수 개수
    int count = std::count_if(numbers.begin(), numbers.end(), IsEven());
    // 짝수 찾기
    auto it = std::find_if(numbers.begin(), numbers.end(), IsEven());
}

완전한 예제: 범위 검증 Functor

실무에서 “입력값이 유효 범위 내인지” 검사하는 함수 객체를 자주 만듭니다. 상태(최소/최대값)를 생성자로 받고, operator()에서 검증합니다.

struct InRange {
    int minVal, maxVal;
    InRange(int min, int max) : minVal(min), maxVal(max) {}
    bool operator()(int x) const {
        return x >= minVal && x <= maxVal;
    }
};
int main() {
    std::vector<int> data = {5, 15, 25, 35, 45};
    // 10~40 범위 내 개수
    int count = std::count_if(data.begin(), data.end(), InRange(10, 40));
    std::cout << count << "\n";  // 3 (15, 25, 35)
}

완전한 예제: 로깅 래퍼 Functor

기존 함수를 감싸서 호출 전후로 로그를 남기는 함수 객체입니다. std::function으로 원본을 저장하고, operator()에서 로깅 후 위임합니다.

template <typename R, typename....Args>
class LoggingWrapper {
    std::function<R(Args...)> wrapped;
    std::string name;
public:
    LoggingWrapper(std::function<R(Args...)> f, const std::string& n)
        : wrapped(f), name(n) {}
    R operator()(Args....args) {
        std::cout << "[LOG] Calling " << name << "\n";
        R result = wrapped(args...);
        std::cout << "[LOG] " << name << " returned\n";
        return result;
    }
};
int add(int a, int b) { return a + b; }
int main() {
    LoggingWrapper<int, int, int> loggedAdd(add, "add");
    std::cout << loggedAdd(3, 5) << "\n";
    // [LOG] Calling add
    // [LOG] add returned
    // 8
}

완전한 예제: 재시도 Functor

실패 시 N번까지 재시도하는 함수 객체입니다. 네트워크 호출, 파일 I/O 등에 유용합니다.

template <typename Func>
class Retry {
    Func func;
    int maxAttempts;
public:
    Retry(Func f, int max = 3) : func(f), maxAttempts(max) {}
    template <typename....Args>
    auto operator()(Args&&....args) -> decltype(func(std::forward<Args>(args)...)) {
        for (int i = 0; i < maxAttempts; ++i) {
            try {
                return func(std::forward<Args>(args)...);
            } catch (...) {
                if (i == maxAttempts - 1) throw;
                std::cout << "Retry " << (i + 1) << "/" << maxAttempts << "\n";
            }
        }
        throw std::runtime_error("Retry exhausted");
    }
};

3. std::bind

기본 사용법

int add(int a, int b, int c) {
    return a + b + c;
}
// 첫 번째 인자를 10으로 고정
auto add10 = std::bind(add, 10, std::placeholders::_1, std::placeholders::_2);
int result = add10(5, 3);  // add(10, 5, 3) = 18

인자 순서 변경

int subtract(int a, int b) {
    return a - b;
}
// 인자 순서 바꾸기
auto reverseSub = std::bind(subtract, std::placeholders::_2, std::placeholders::_1);
std::cout << subtract(10, 3) << "\n";      // 7
std::cout << reverseSub(10, 3) << "\n";    // -7 (3 - 10)

멤버 함수 바인딩

class Printer {
public:
    void print(const std::string& msg) {
        std::cout << "Message: " << msg << "\n";
    }
};
int main() {
    Printer printer;
    // 멤버 함수 바인딩
    auto boundPrint = std::bind(&Printer::print, &printer, std::placeholders::_1);
    boundPrint("Hello");  // Message: Hello
}

람다 vs bind

int x = 10;
// bind 사용
auto func1 = std::bind( { return a + b; }, x, std::placeholders::_1);
// ✅ 람다가 더 명확 (권장)
auto func2 = [x](int b) { return x + b; };
std::cout << func1(5) << "\n";  // 15
std::cout << func2(5) << "\n";  // 15

람다를 권장하는 이유: std::bind_1, _2 같은 자리 표시자가 나열되어 있어 의도가 한눈에 들어오지 않고, 인자 순서를 바꿀 때 실수하기 쉽습니다. 람다는 “어떤 값을 캡처해서 어떤 인자로 넘길지”가 그대로 드러나서 가독성과 유지보수에 유리합니다. 성능 면에서도 람다는 인라인되기 쉬운 반면, bind로 만든 호출 객체는 추가 간접 호출이 생길 수 있습니다.

4. 콜백 패턴

이벤트 핸들러

class EventEmitter {
    std::vector<std::function<void(const std::string&)>> listeners;
public:
    void on(std::function<void(const std::string&)> callback) {
        listeners.push_back(callback);
    }
    void emit(const std::string& event) {
        for (auto& listener : listeners) {
            listener(event);
        }
    }
};
int main() {
    EventEmitter emitter;
    emitter.on( {
        std::cout << "Listener 1: " << event << "\n";
    });
    emitter.on( {
        std::cout << "Listener 2: " << event << "\n";
    });
    emitter.emit("click");
    // Listener 1: click
    // Listener 2: click
}

비동기 콜백

#include <thread>
#include <chrono>
void asyncOperation(std::function<void(int)> callback) {
    std::thread([callback]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        callback(42);  // 결과 전달
    }).detach();
}
int main() {
    std::cout << "Starting...\n";
    asyncOperation( {
        std::cout << "Result: " << result << "\n";
    });
    std::cout << "Waiting...\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
}

에러 핸들링

using SuccessCallback = std::function<void(int)>;
using ErrorCallback = std::function<void(const std::string&)>;
void divide(int a, int b, SuccessCallback onSuccess, ErrorCallback onError) {
    if (b == 0) {
        onError("Division by zero");
    } else {
        onSuccess(a / b);
    }
}
int main() {
    divide(10, 2,
         {
            std::cout << "Success: " << result << "\n";
        },
         {
            std::cerr << "Error: " << error << "\n";
        });
}

5. 실전 활용

패턴 1: 전략 패턴

class Sorter {
    std::function<bool(int, int)> comparator;
public:
    void setStrategy(std::function<bool(int, int)> comp) {
        comparator = comp;
    }
    void sort(std::vector<int>& vec) {
        std::sort(vec.begin(), vec.end(), comparator);
    }
};
int main() {
    std::vector<int> numbers = {5, 2, 8, 1, 9};
    Sorter sorter;
    // 오름차순
    sorter.setStrategy( { return a < b; });
    sorter.sort(numbers);
    // 내림차순
    sorter.setStrategy( { return a > b; });
    sorter.sort(numbers);
}

패턴 2: 명령 패턴

class Command {
public:
    virtual ~Command() = default;
    virtual void execute() = 0;
};
class CommandManager {
    std::vector<std::function<void()>> commands;
public:
    void addCommand(std::function<void()> cmd) {
        commands.push_back(cmd);
    }
    void executeAll() {
        for (auto& cmd : commands) {
            cmd();
        }
    }
};
int main() {
    CommandManager manager;
    manager.addCommand( { std::cout << "Command 1\n"; });
    manager.addCommand( { std::cout << "Command 2\n"; });
    manager.addCommand( { std::cout << "Command 3\n"; });
    manager.executeAll();
}

패턴 3: 체인 패턴

class Pipeline {
    std::vector<std::function<int(int)>> stages;
public:
    Pipeline& addStage(std::function<int(int)> stage) {
        stages.push_back(stage);
        return *this;
    }
    int execute(int input) {
        int result = input;
        for (auto& stage : stages) {
            result = stage(result);
        }
        return result;
    }
};
int main() {
    Pipeline pipeline;
    pipeline
        .addStage( { return x * 2; })
        .addStage( { return x + 10; })
        .addStage( { return x * x; });
    int result = pipeline.execute(5);  // ((5*2)+10)^2 = 400
    std::cout << result << "\n";
}

패턴 4: 메모이제이션

template <typename R, typename....Args>
class Memoized {
    std::function<R(Args...)> func;
    mutable std::map<std::tuple<Args...>, R> cache;
public:
    Memoized(std::function<R(Args...)> f) : func(f) {}
    R operator()(Args....args) const {
        auto key = std::make_tuple(args...);
        auto it = cache.find(key);
        if (it != cache.end()) {
            return it->second;
        }
        R result = func(args...);
        cache[key] = result;
        return result;
    }
};
int fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
    Memoized<int, int> memoFib(fibonacci);
    std::cout << memoFib(40) << "\n";  // 빠름 (캐시됨)
}

패턴 5: 타이머

class Timer {
    std::function<void()> callback;
    std::chrono::milliseconds interval;
    std::thread thread;
    std::atomic<bool> running{false};
public:
    Timer(std::chrono::milliseconds ms, std::function<void()> cb)
        : interval(ms), callback(cb) {}
    void start() {
        running = true;
        thread = std::thread([this]() {
            while (running) {
                std::this_thread::sleep_for(interval);
                if (running) callback();
            }
        });
    }
    void stop() {
        running = false;
        if (thread.joinable()) thread.join();
    }
    ~Timer() {
        stop();
    }
};
int main() {
    int count = 0;
    Timer timer(std::chrono::milliseconds(100), [&count]() {
        std::cout << "Tick " << ++count << "\n";
    });
    timer.start();
    std::this_thread::sleep_for(std::chrono::seconds(1));
    timer.stop();
}

6. 자주 발생하는 오류와 해결법

오류 1: 빈 std::function 호출

증상: std::bad_function_call 예외 또는 크래시 원인: std::function에 아무것도 대입하지 않은 상태에서 func()를 호출함

// ❌ 위험
std::function<void()> func;
func();  // std::bad_function_call
// ✅ 호출 전 검사
if (func) {
    func();
}

오류 2: 시그니처 불일치

증상: 컴파일 에러 “no matching function for call” 원인: std::function에 넣으려는 callable의 시그니처가 맞지 않음

// ❌ 반환 타입 불일치
std::function<void(int)> f =  { return x * 2; };  // int 반환
// ✅ 시그니처 일치
std::function<int(int)> f =  { return x * 2; };

오류 3: 람다 캡처로 인한 dangling reference

증상: 크래시 또는 undefined behavior 원인: 람다가 참조로 캡처한 지역 변수가 스코프를 벗어난 뒤 콜백이 호출됨

// ❌ 위험
std::function<void()> callback;
{
    int local = 42;
    callback = [&local]() { std::cout << local << "\n"; };
}
callback();  // local은 이미 소멸됨!
// ✅ 값으로 캡처
int local = 42;
callback = [local]() { std::cout << local << "\n"; };

오류 4: std::bind와 객체 수명

증상: 크래시 (use-after-free) 원인: std::bind로 묶은 객체 포인터가 가리키는 객체가 먼저 소멸됨

// ❌ 위험
std::function<void()> bound;
{
    Printer printer;
    bound = std::bind(&Printer::print, &printer, "Hello");
}
bound();  // printer는 이미 소멸됨!
// ✅ 객체 수명이 callback보다 길어야 함
Printer printer;
auto bound = std::bind(&Printer::print, &printer, "Hello");
bound();

오류 5: 재귀 호출에서 std::function 대입

증상: 무한 재귀 또는 잘못된 동작 원인: 재귀 함수를 std::function에 담을 때, 아직 초기화되지 않은 자기 자신을 호출하려 함

// ❌ 잘못된 재귀 람다
std::function<int(int)> factorial;
factorial = [&factorial](int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
};
// ✅ 일반 함수 사용
int factorialImpl(int n) {
    if (n <= 1) return 1;
    return n * factorialImpl(n - 1);
}
std::function<int(int)> factorial = factorialImpl;

7. 성능 비교와 선택 가이드

std::function vs 템플릿 vs 함수 포인터

방식인라인힙 할당타입 소거유연성
std::function경우에 따라높음
템플릿 template<typename F>높음
함수 포인터낮음

벤치마크 개념 (1억 회 호출 가정)

// std::function: ~수백 ns/호출 (간접 호출, 인라인 어려움)
std::function<int(int)> f =  { return x * 2; };
for (int i = 0; i < 100000000; ++i) sum += f(i);
// 템플릿: ~수 ns/호출 (인라인 가능)
template <typename F>
void loop(F&& f) {
    for (int i = 0; i < 100000000; ++i) sum += f(i);
}
loop( { return x * 2; });
// 함수 포인터: std::function보다 약간 빠를 수 있음
int (*fp)(int) =  { return x * 2; };  // 캡처 없는 람다만

선택 가이드

  • 런타임에 콜백을 바꿔야 함std::function
  • 컨테이너에 여러 타입의 callable 저장std::function
  • 핫 루프, 초당 수백만 호출 → 템플릿으로 callable 직접 받기
  • 캡처 없는 람다만 → 함수 포인터도 가능 (C++11)

작은 객체 최적화 (SBO)

// 작은 람다: 힙 할당 없음 (대부분의 구현에서)
std::function<int(int)> small =  { return x * 2; };
// 큰 람다: 힙 할당 발생
int data[100];
std::function<int(int)> large = [data](int x) { return x + data[0]; };

SBO가 의미하는 것: std::function은 내부에 작은 버퍼를 갖고 있어서, 저장할 호출 객체가 그 크기 이하면 힙 할당 없이 그 버퍼에 넣습니다. 캡처가 많은 람다처럼 크기가 크면 힙에 할당하고 포인터만 들고 있어서, 호출 시 한 번 더 간접 참조가 들어가고 할당/해제 비용도 생깁니다. 그래서 “캡처를 최소화한 람다”를 넣을수록 std::function 오버헤드가 줄어듭니다.

8. 프로덕션 패턴

패턴 1: 옵셔널 콜백

콜백이 없을 수 있는 API에서, 호출 전 null 체크를 반복하지 않도록 래퍼를 둡니다.

class OptionalCallback {
    std::function<void(int)> callback;
public:
    void set(std::function<void(int)> cb) { callback = std::move(cb); }
    void invoke(int value) {
        if (callback) callback(value);
    }
};

패턴 2: 스레드 안전 이벤트 큐

작업 스레드에서 이벤트를 큐에 넣고, 메인 스레드에서 콜백을 실행하는 패턴입니다.

#include <queue>
#include <mutex>
#include <condition_variable>
class EventQueue {
    std::queue<std::function<void()>> queue;
    std::mutex mtx;
    std::condition_variable cv;
public:
    void post(std::function<void()> task) {
        std::lock_guard<std::mutex> lock(mtx);
        queue.push(std::move(task));
        cv.notify_one();
    }
    void processOne() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this] { return !queue.empty(); });
        auto task = std::move(queue.front());
        queue.pop();
        lock.unlock();
        task();
    }
};

패턴 3: 데코레이터 체인

여러 콜백을 순서대로 실행하는 체인입니다. 미들웨어, 필터 패턴에 활용됩니다.

using Middleware = std::function<std::function<void()>(std::function<void()>)>;
std::function<void()> applyMiddleware(
    std::function<void()> handler,
    std::vector<Middleware> middlewares)
{
    for (auto it = middlewares.rbegin(); it != middlewares.rend(); ++it) {
        handler = (*it)(std::move(handler));
    }
    return handler;
}
// 사용 예
auto logged =  {
    return [next]() {
        std::cout << "Before\n";
        next();
        std::cout << "After\n";
    };
};
auto final = applyMiddleware( { std::cout << "Handler\n"; }, {logged});
final();  // Before, Handler, After

패턴 4: 타임아웃 래퍼

지정 시간 내에 완료되지 않으면 에러 콜백을 호출하는 패턴입니다.

#include <future>
void withTimeout(
    std::function<void()> task,
    std::chrono::milliseconds timeout,
    std::function<void()> onTimeout)
{
    auto future = std::async(std::launch::async, std::move(task));
    if (future.wait_for(timeout) == std::future_status::timeout) {
        onTimeout();
    } else {
        future.get();
    }
}

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

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

  • C++ 가변 인자 템플릿 | Variadic Templates와 Fold Expression
  • C++ 컴파일 타임 최적화 | constexpr·PCH·모듈·ccache·Unity 빌드 [#15-3]
  • C++ auto와 decltype | 타입 추론으로 코드 간결하게 만드는 방법

이 글에서 다루는 키워드 (관련 검색어)

C++ std::function, 함수 객체, 콜백, 전략 패턴, std::bind, functor 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목용도장점단점
std::function콜백 저장유연함오버헤드
함수 객체상태 + 동작빠름코드 많음
람다간단한 로직간결함재사용 어려움
std::bind인자 고정편리함가독성 낮음

초보자를 위한 체크리스트

  • std::function너무 큰 캡처 람다를 넣어 할당이 과하지 않은지 확인했는가?
  • nullptr 할당 후 호출하지 않도록 수명·초기화를 정했는가?
  • 같은 일을 람다만으로 끝낼 수 있는지 한 번 검토했는가? (bind 남용 방지)

💡 초보자 팁: 본문 6. 자주 발생하는 오류·7. 성능 비교와 선택 가이드를 함께 보세요.

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. C++ std::function 완벽 가이드. 함수·람다·함수 객체를 변수에 저장, 콜백 패턴 구현, 전략 패턴(Strategy Pattern), operator() 오버로딩, std::bind 사용법, 성능 오버헤… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다. 한 줄 요약: std::function으로 콜백·전략을 타입에 구애받지 않고 넘길 수 있습니다. 다음으로 이동 의미론(#14-1)를 읽어보면 좋습니다. 이전 글: C++ 실전 가이드 #13-1: 람다 표현식 다음 글: C++ 실전 가이드 #14-1: 이동 의미론 — std::move와 rvalue 레퍼런스를 다룹니다. 핵심 원칙:

  1. 콜백 저장은 std::function
  2. 성능 중요하면 템플릿
  3. 간단한 로직은 람다
  4. bind보다 람다 선호
  5. 상태 필요하면 함수 객체

관련 글

  • C++ 람다 표현식 | [=]·[&] 캡처와 sort·find_if에서 람다 활용법
  • C++ vector 기초 완벽 가이드 | 초기화·연산·용량 관리와 실전 패턴
  • C++ map·set 완벽 가이드 | ordered vs unordered· 커스텀 키
  • C++ 컨테이너 선택 가이드 | vector/list/deque/map/set 상황별 선택과 성능 최적화
  • C++ 함수 객체(Functor) 완벽 가이드 | operator·상태 보유

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ std::function | 콜백·전략 패턴과 함수 객체」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ std::function | 콜백·전략 패턴과 함수 객체」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.