[2026] C++ 비동기 작업과 Coroutine | co_await로 콜백 지옥 탈출하기 [#23-3]

[2026] C++ 비동기 작업과 Coroutine | co_await로 콜백 지옥 탈출하기 [#23-3]

이 글의 핵심

C++20 코루틴으로 비동기 작업을 co_await하고, Task 타입을 설계하며, Asio와 연동하는 기본 패턴을 다룹니다. 콜백 지옥 해결, 에러 처리, 수명 관리, 성능 비교, 베스트 프랙티스, 프로덕션 패턴까지 실전 가이드.

들어가며: “비동기 코드가 콜백 지옥이에요”

콜백 지옥이란?

네트워크 요청을 보낸 뒤, 응답이 오면 파싱하고, 그 결과로 다른 API를 호출하고, 그 결과로 DB에 저장하는 식의 비동기 연쇄를 작성할 때, 전통적인 콜백 방식은 코드가 깊어지고 읽기 어려워집니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 콜백 지옥: HTTP 요청 → 파싱 → DB 저장
void fetchUserData(const std::string& userId) {
    httpGet("/api/user/" + userId, [userId](error_code ec, std::string body) {
        if (ec) { /* 에러 처리 */ return; }
        parseJson(body, [userId, body](error_code ec, User user) {
            if (ec) { /* 에러 처리 */ return; }
            dbSave(user, [userId](error_code ec) {
                if (ec) { /* 에러 처리 */ return; }
                notifyUser(userId);  // 4단계 중첩!
            });
        });
    });
}

문제점:

  • 중첩 람다가 깊어질수록 들여쓰기와 스코프가 복잡해짐
  • 에러 처리가 각 단계마다 반복되고, early return 패턴이 산재함
  • 변수 캡처[userId, body]처럼 늘어나고, 수명 관리가 어려움
  • 디버깅 시 콜 스택이 끊겨서 흐름 추적이 힘듦 코루틴으로 해결: 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ co_await: 동기 코드처럼 읽는 순서대로 실행
// 실행 예제
Task<void> fetchUserData(const std::string& userId) {
    std::string body = co_await httpGet("/api/user/" + userId);
    User user = co_await parseJson(body);
    co_await dbSave(user);
    notifyUser(userId);
}

코루틴의 장점:

  • 한 줄씩 위에서 아래로 읽히는 흐름
  • 에러 처리수명 관리가 단순해짐
  • co_await 시 해당 코루틴만 일시 정지하고, 스레드는 블로킹되지 않음 처음 코루틴을 접할 때 “co_await 한 번에 스레드가 블로킹되나?”라고 생각할 수 있습니다. 블로킹되지 않습니다. co_await 시 그 코루틴만 일시 정지하고, 이벤트 루프(또는 io_context)는 다른 핸들러를 계속 실행합니다. 완료되면 해당 코루틴이 재개되므로, 논블로킹 이벤트 루프 모델은 그대로 유지됩니다. 아래 코드는 mermaid를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
flowchart LR
  subgraph callback[콜백 방식]
    C1[요청] --> C2[콜백1]
    C2 --> C3[콜백2]
    C3 --> C4[콜백3]
    C4 --> C5[콜백4]
  end
  subgraph coroutine[코루틴 방식]
    R1[요청] --> R2[co_await]
    R2 --> R3[co_await]
    R3 --> R4[co_await]
    R4 --> R5[완료]
  end

이 글을 읽으면:

  • co_await가 동작하는 방식을 이해할 수 있습니다.
  • 간단한 Task 타입을 설계할 수 있습니다.
  • Asio와 연동하는 실전 패턴을 알 수 있습니다.
  • 수명·에러·성능 등 실전 주의사항을 파악할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  1. Awaitable이란
  2. Task 타입 완전 구현
  3. co_await 연산자 오버로딩
  4. Asio 연동 예제
  5. 코루틴 에러 처리
  6. 자주 발생하는 실수
  7. 성능 비교: 콜백 vs 코루틴
  8. 프로덕션 패턴

1. Awaitable이란

세 가지 메서드

co_await expr에서 expr의 타입(또는 operator co_await 결과)이 제공해야 하는 것: 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct MyAwaitable {
    // 1. 이미 완료되었으면 true → 일시 정지 없이 바로 await_resume
    bool await_ready() const {
        return false;  // true면 일시 정지 안 함
    }
    // 2. 일시 정지 시 호출. h: 현재 코루틴 핸들
    //    완료 시 h.resume() 호출하면 재개
    void await_suspend(std::coroutine_handle<> h) {
        // 비동기 작업 시작, 완료 콜백에서 h.resume() 호출
    }
    // 3. 재개 시 호출. co_await 식의 결과값
    int await_resume() {
        return 42;
    }
};

co_await 동작 흐름

다음은 mermaid를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

sequenceDiagram
    participant C as 코루틴
    participant A as Awaitable
    participant S as 스케줄러
    C->>A: await_ready()
    alt 이미 완료
        A-->>C: true
        C->>A: await_resume()
    else 미완료
        A-->>C: false
        C->>A: await_suspend(handle)
        A->>S: 비동기 작업 등록
        Note over C: 일시 정지
        S->>A: 완료 콜백
        A->>C: handle.resume()
        C->>A: await_resume()
    end

suspend_always / suspend_never

표준 라이브러리에서 제공하는 유틸리티: 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct suspend_always {
    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume() const noexcept {}
};
struct suspend_never {
    bool await_ready() const noexcept { return true; }
    void await_suspend(std::coroutine_handle<>) const noexcept {}
    void await_resume() const noexcept {}
};
  • suspend_always: 항상 일시 정지
  • suspend_never: 절대 일시 정지하지 않음 (초기화 완료 시점 등)

2. Task 타입 완전 구현

반환값 있는 Task (동작 가능한 구현)

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <coroutine>
#include <exception>
#include <utility>
template <typename T>
class Task {
public:
    struct promise_type {
        T value;
        std::coroutine_handle<> continuation = nullptr;
        std::exception_ptr exception;
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept {
            if (continuation) continuation.resume();
            return {};
        }
        void return_value(T v) { value = std::move(v); }
        void unhandled_exception() { exception = std::current_exception(); }
    };
    struct Awaiter {
        std::coroutine_handle<promise_type> handle;
        bool await_ready() const { return handle.done(); }
        void await_suspend(std::coroutine_handle<> h) {
            handle.promise().continuation = h;
            if (!handle.done()) return;
            h.resume();
        }
        T await_resume() {
            if (handle.promise().exception)
                std::rethrow_exception(handle.promise().exception);
            return std::move(handle.promise().value);
        }
    };
    Awaiter operator co_await() { return Awaiter{handle_}; }
    T get() {
        while (!handle_.done()) handle_.resume();
        if (handle_.promise().exception)
            std::rethrow_exception(handle_.promise().exception);
        return std::move(handle_.promise().value);
    }
    ~Task() { handle_.destroy(); }
    Task(Task&& o) noexcept : handle_(o.handle_) { o.handle_ = nullptr; }
    Task& operator=(Task&& o) noexcept {
        if (this != &o) {
            handle_.destroy();
            handle_ = o.handle_;
            o.handle_ = nullptr;
        }
        return *this;
    }
private:
    explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
    std::coroutine_handle<promise_type> handle_;
};

void 반환 Task

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

template <>
class Task<void> {
public:
    struct promise_type {
        std::coroutine_handle<> continuation = nullptr;
        std::exception_ptr exception;
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept {
            if (continuation) continuation.resume();
            return {};
        }
        void return_void() {}
        void unhandled_exception() { exception = std::current_exception(); }
    };
    struct Awaiter {
        std::coroutine_handle<promise_type> handle;
        bool await_ready() const { return handle.done(); }
        void await_suspend(std::coroutine_handle<> h) {
            handle.promise().continuation = h;
            if (!handle.done()) return;
            h.resume();
        }
        void await_resume() {
            if (handle.promise().exception)
                std::rethrow_exception(handle.promise().exception);
        }
    };
    Awaiter operator co_await() { return Awaiter{handle_}; }
    void get() {
        while (!handle_.done()) handle_.resume();
        if (handle_.promise().exception)
            std::rethrow_exception(handle_.promise().exception);
    }
    ~Task() { handle_.destroy(); }
    Task(Task&&) = default;
    Task& operator=(Task&&) = default;
private:
    explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
    std::coroutine_handle<promise_type> handle_;
};

사용 예시

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Task<int> fetchAndParse() {
    std::string raw = co_await httpGet("https://example.com");
    int value = co_await parseAsync(raw);
    co_return value;
}
Task<void> mainFlow() {
    int x = co_await fetchAndParse();
    std::cout << "result: " << x << "\n";
}

실제 프로덕션에서는 cppcoro, libunifex 등 라이브러리 Task를 쓰거나, 스케줄러·executor 연동을 추가하는 것이 일반적입니다.

완전한 co_await 연쇄 예제 (Task + Awaitable)

Task(위 섹션 구현)와 커스텀 Awaitable을 조합한 비동기 연쇄 예제입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// sleep Awaitable: await_suspend에서 별도 스레드로 sleep 후 h.resume() 호출
struct SleepAwaitable {
    std::chrono::milliseconds duration;
    bool await_ready() const { return duration.count() <= 0; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, d = duration]() {
            std::this_thread::sleep_for(d);
            h.resume();
        }).detach();
    }
    void await_resume() {}
};
// 비동기 연쇄: fetch → parse → save (한 줄씩 순차 실행)
Task<int> fetchParseAndSave(const std::string& url) {
    co_await SleepAwaitable{std::chrono::milliseconds(100)};
    std::string body = "simulated_response";
    co_await SleepAwaitable{std::chrono::milliseconds(50)};
    int value = 42;
    co_await SleepAwaitable{std::chrono::milliseconds(50)};
    co_return value;
}
Task<void> mainFlow() {
    int result = co_await fetchParseAndSave("https://api.example.com/data");
    std::cout << "result: " << result << "\n";
}

핵심: Task<T>operator co_await로 다른 코루틴이 co_await fetchParseAndSave(...) 가능. 연쇄가 한 줄씩 읽히며, 각 co_await 시점에 해당 코루틴만 일시 정지합니다.

3. co_await 연산자 오버로딩

operator co_await

co_await expr에서 exproperator co_await를 제공하면, 그 결과가 Awaitable로 사용됩니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct DelayedValue {
    int value;
    std::chrono::milliseconds delay;
    struct Awaiter {
        int value;
        std::chrono::milliseconds delay;
        bool await_ready() const { return delay.count() == 0; }
        void await_suspend(std::coroutine_handle<> h) {
            // 타이머 스케줄: delay 후 h.resume() 호출
            scheduleTimer(delay, [h]() { h.resume(); });
        }
        int await_resume() { return value; }
    };
    Awaiter operator co_await() { return Awaiter{value, delay}; }
};
Task<int> example() {
    auto v = co_await DelayedValue{42, std::chrono::milliseconds(100)};
    co_return v;
}

사용자 정의 Awaitable

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 로그를 남기는 Awaitable 래퍼
template <typename T>
struct LoggingAwaitable {
    T inner;
    struct Awaiter {
        T inner;
        bool await_ready() const { return inner.await_ready(); }
        template <typename Promise>
        auto await_suspend(std::coroutine_handle<Promise> h) {
            std::cout << "suspending...\n";
            return inner.await_suspend(h);
        }
        auto await_resume() {
            std::cout << "resuming...\n";
            return inner.await_resume();
        }
    };
    Awaiter operator co_await() { return Awaiter{std::move(inner)}; }
};

4. Asio 연동 예제

boost::asio::awaitable

Boost.Asio 1.70+는 awaitableuse_awaitable 토큰을 제공합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <boost/asio.hpp>
#include <boost/asio/use_awaitable.hpp>
boost::asio::awaitable<void> session(boost::asio::ip::tcp::socket socket) {
    std::array<char, 1024> buf;
    try {
        for (;;) {
            std::size_t n = co_await socket.async_read_some(
                boost::asio::buffer(buf),
                boost::asio::use_awaitable);
            co_await boost::asio::async_write(socket,
                boost::asio::buffer(buf.data(), n),
                boost::asio::use_awaitable);
        }
    } catch (const boost::system::system_error& e) {
        if (e.code() != boost::asio::error::eof)
            std::cerr << "session error: " << e.what() << "\n";
    }
}
boost::asio::awaitable<void> listener(boost::asio::ip::tcp::acceptor& acceptor) {
    for (;;) {
        auto socket = co_await acceptor.async_accept(boost::asio::use_awaitable);
        auto ex = socket.get_executor();
        boost::asio::co_spawn(ex, session(std::move(socket)), boost::asio::detached);
    }
}
int main() {
    boost::asio::io_context io;
    boost::asio::ip::tcp::acceptor acceptor(io,
        {boost::asio::ip::tcp::v4(), 8080});
    boost::asio::co_spawn(io, listener(acceptor), boost::asio::detached);
    io.run();
}
  • use_awaitable: 비동기 연산을 co_await 가능하게 만드는 토큰
  • co_spawn: 코루틴을 executor에서 실행
  • detached: 코루틴 완료를 기다리지 않음

Strand와 연동

auto ex = boost::asio::make_strand(io.get_executor());
boost::asio::co_spawn(ex, session(std::move(socket)), boost::asio::detached);
  • session의 모든 co_await 재개가 같은 strand에서 실행됩니다.
  • 스레드 안전성을 보장하면서 락 없이 직렬화할 수 있습니다.

완전한 비동기 코루틴 예제: Echo 서버 (컴파일 가능)

아래는 Boost.Asio와 C++20 코루틴으로 작성한 Echo 서버 전체 코드입니다. 클라이언트가 보낸 데이터를 그대로 돌려보냅니다.

환경 요구사항

  • C++20 (g++ -std=c++20 또는 clang++ -std=c++20)
  • Boost 1.70+ (apt install libboost-all-dev 또는 vcpkg)

전체 소스 코드

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <boost/asio.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <iostream>
// Echo 세션: 클라이언트가 보낸 데이터를 그대로 돌려보냄
boost::asio::awaitable<void> echoSession(
    boost::asio::ip::tcp::socket socket)
{
    std::array<char, 1024> buf;
    try {
        for (;;) {
            std::size_t n = co_await socket.async_read_some(
                boost::asio::buffer(buf),
                boost::asio::use_awaitable);
            if (n == 0) break;
            co_await boost::asio::async_write(socket,
                boost::asio::buffer(buf.data(), n),
                boost::asio::use_awaitable);
        }
    } catch (const boost::system::system_error& e) {
        if (e.code() != boost::asio::error::eof) {
            std::cerr << "session error: " << e.what() << "\n";
        }
    }
}
// 리스너: 연결 수락 후 각 세션을 별도 코루틴으로 실행
boost::asio::awaitable<void> listener(
    boost::asio::ip::tcp::acceptor& acceptor)
{
    for (;;) {
        auto socket = co_await acceptor.async_accept(boost::asio::use_awaitable);
        auto ex = socket.get_executor();
        boost::asio::co_spawn(ex, echoSession(std::move(socket)),
            boost::asio::detached);
    }
}
int main() {
    boost::asio::io_context io;
    boost::asio::ip::tcp::acceptor acceptor(io,
        {boost::asio::ip::tcp::v4(), 8080});
    boost::asio::co_spawn(io, listener(acceptor), boost::asio::detached);
    std::cout << "Echo server listening on port 8080\n";
    io.run();
    return 0;
}

빌드 및 실행

# g++ (Boost 설치 필요)
g++ -std=c++20 -o echo_server echo_server.cpp -lboost_system -lpthread
# 실행
./echo_server
# 다른 터미널에서 테스트
echo "hello" | nc localhost 8080
# 출력: hello

5. 코루틴 에러 처리

try/catch

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Task<std::string> fetchWithRetry(const std::string& url) {
    for (int i = 0; i < 3; ++i) {
        try {
            std::string result = co_await httpGet(url);
            co_return result;
        } catch (const std::exception& e) {
            std::cerr << "attempt " << (i + 1) << " failed: " << e.what() << "\n";
            if (i == 2) throw;
            co_await sleep(std::chrono::seconds(1));
        }
    }
    co_return "";  // unreachable
}

promise_type::unhandled_exception

void unhandled_exception() {
    exception = std::current_exception();
}
  • 코루틴 내부에서 예외가 던져지면 unhandled_exception이 호출됩니다.
  • exception을 저장해 두고, await_resume 또는 get에서 std::rethrow_exception으로 전파합니다.

error_code vs 예외

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// Asio: use_awaitable은 기본적으로 예외를 던짐
try {
    co_await socket.async_read_some(buf, boost::asio::use_awaitable);
} catch (const boost::system::system_error& e) {
    if (e.code() == boost::asio::error::eof) { /* 정상 종료 */ }
}
// error_code로 받고 싶다면
co_await socket.async_read_some(buf,
    boost::asio::redirect_error(boost::asio::use_awaitable, ec));
if (ec) { /* 처리 */ }

6. 자주 발생하는 실수

6.1 Dangling 참조

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 위험: s는 참조인데, 호출하는 쪽의 지역 변수가 스코프를 벗어나면
//    코루틴이 재개될 때 이미 파괴된 객체를 참조
Task<void> bad(const std::string& s) {
    co_await someAsyncOp();
    std::cout << s << "\n";  // UB: s가 이미 파괴됐을 수 있음
}
// ✅ 올바름: 값으로 복사
Task<void> good(std::string s) {
    co_await someAsyncOp();
    std::cout << s << "\n";
}

원인: co_await 이후 재개 시점에, 참조 인자로 받은 객체의 수명이 끝났을 수 없음. 해결: 비동기 함수에서는 값으로 복사하거나, shared_ptr로 수명을 확장합니다.

6.2 코루틴 핸들 수명

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 위험: Task가 파괴되면 handle_도 파괴됨
//    아직 suspend된 코루틴이면 메모리 누수 또는 UB
Task<int> getTask() {
    return computeAsync();  // Task 반환
}
// 호출자가 getTask()의 반환값을 받지 않고 버리면?
// ✅ 올바름: Task를 반드시 소유하거나 co_await
void main() {
    auto t = getTask();  // Task 소유
    int x = t.get();     // 또는 co_await
}

원인: coroutine_handle이 파괴되면 코루틴 프레임도 함께 파괴되어야 합니다. Task를 버리면 handle_.destroy()가 호출되지 않을 수 있습니다.

6.3 스레드 안전성

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 위험: 재개가 다른 스레드에서 일어나면, 공유 데이터 접근 시 data race
Task<void> unsafe() {
    static int counter = 0;
    co_await asyncOnOtherThread();
    ++counter;  // data race!
}
// ✅ 올바름: strand로 직렬화하거나, 락 사용
boost::asio::co_spawn(strand, unsafe(), boost::asio::detached);

6.4 반환된 객체의 이동

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 위험: Task는 이동 전용. 복사하면 안 됨
Task<int> t = compute();
Task<int> t2 = t;  // 에러 (복사 생성자 삭제)
// ✅ 올바름
Task<int> t2 = std::move(t);

6.5 co_await 결과 무시, 루프 내 반복 생성, executor 누락

  • co_await 결과 무시: 연결 실패 등 예외를 try/catch로 처리하지 않으면 호출자까지 전파됩니다.
  • 루프 내 반복 생성: processOne이 Task를 반환하면 매 반복마다 새 프레임이 할당됩니다. Awaitable만 반환하도록 하거나, 단일 코루틴 안에서 루프를 돌리세요.
  • executor 누락: boost::asio::steady_timer timer(io) 대신 co_await this_coro::executor로 현재 executor를 얻어 타이머를 생성하세요.

6.6 요약 표

실수원인해결
Dangling 참조co_await 후 재개 시 참조 대상 파괴값 복사, shared_ptr
핸들 수명Task 버림 → destroy 미호출Task 소유 또는 co_await
Data race재개가 다른 스레드에서strand, 락
복사 시도Task 이동 전용std::move
co_await 결과 무시에러 감지 불가try/catch, error_code
루프 내 반복 생성불필요한 프레임 할당단일 코루틴 + 루프
executor 누락잘못된 스레드에서 재개this_coro::executor

6.7 promise_type·executor·co_return 경로

실수원인해결
promise_type 누락get_return_object, initial_suspend, final_suspend, return_value/return_void, unhandled_exception 중 하나라도 없으면 컴파일 에러필수 메서드 모두 구현
executor 누락boost::asio::steady_timer timer(io)처럼 io_context 직접 사용 시 재개 스레드 불일치co_await this_coro::executor로 현재 executor 사용
co_return 경로 누락if/else에서 한 경로에만 co_return → UB모든 경로에서 co_return 또는 throw
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ executor 잘못 사용
boost::asio::awaitable<void> bad(boost::asio::io_context& io) {
    boost::asio::steady_timer timer(io);  // io의 executor
    co_await timer.async_wait(boost::asio::use_awaitable);
}
// ✅ 현재 코루틴의 executor 사용
boost::asio::awaitable<void> good() {
    auto ex = co_await boost::asio::this_coro::executor;
    boost::asio::steady_timer timer(ex);
    co_await timer.async_wait(boost::asio::use_awaitable);
}

7. 베스트 프랙티스

7.1 인자 전달: 값 vs 참조

상황권장
작은 타입 (int, 포인터)값으로 전달
std::string, vector값 또는 shared_ptr (참조 시 dangling 위험)
읽기 전용 큰 객체shared_ptr로 전달
다음은 간단한 cpp 코드 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
boost::asio::awaitable<void> process(std::string id, std::shared_ptr<Config> c) {
    co_await asyncOp();
    use(id, *c);
}

7.2 에러 전파 전략

  • 예외: Asio use_awaitable 기본. try/catch로 처리.
  • error_code: redirect_error(use_awaitable, ec)로 예외 대신 ec에 저장. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 예외 방식
try {
    auto data = co_await fetch(url);
    co_return process(data);
} catch (const boost::system::system_error& e) {
    log_error(e.code().message());
    throw;
}
// error_code 방식 (예외 비활성화 환경)
boost::system::error_code ec;
co_await socket.async_read_some(buf,
    boost::asio::redirect_error(boost::asio::use_awaitable, ec));
if (ec) { handle_error(ec); co_return; }

7.3 코루틴 수명 관리 체크리스트

  • Task/awaitable을 반환하는 함수는 반환값을 반드시 소유하거나 co_await
  • co_spawn(..., detached) 사용 시 코루틴이 자기 수명을 책임지도록 설계 (내부에서 shared_ptr로 공유 상태 유지)
  • 취소가 필요하면 cancellation_signal 또는 플래그로 주기적으로 확인

7.4 스레드 안전성

  • 단일 스레드 io_context: 추가 동기화 불필요
  • 멀티 스레드 io_context: 공유 데이터 접근 시 strand 또는 mutex 사용
auto strand = boost::asio::make_strand(io.get_executor());
boost::asio::co_spawn(strand, session(socket), boost::asio::detached);

7.5 로깅·디버깅

  • 재개 시점 로깅: await_suspend 진입, await_resume 직전에 로그
  • 코루틴 ID: 디버깅 시 각 코루틴에 고유 ID 부여해 흐름 추적
  • 프로파일링: await_ready로 불필요한 일시 정지 제거

8. 성능 비교: 콜백 vs 코루틴

오버헤드

항목콜백코루틴
힙 할당람다마다 (캡처 많을 때)코루틴 프레임 1회
스택호출마다 새 스택 프레임프레임은 힙에 저장
재개 비용함수 포인터 호출handle.resume()
코드 크기작음promise_type 등으로 증가
일반적인 결과:
  • 단순 연쇄: 코루틴이 약간의 오버헤드(프레임 할당) 있으나, 가독성·유지보수성 이득이 큼
  • 깊은 중첩: 코루틴이 메모리 사용이 더 예측 가능 (프레임 한 번)
  • 고성능 핫경로: 콜백이 미세하게 유리할 수 있으나, 대부분의 경우 차이 무시 가능

요약

상황권장
새 비동기 코드코루틴 (가독성·에러 처리)
기존 콜백 코드점진적 마이그레이션
극한 성능프로파일링 후 결정

성능 최적화 팁

1. await_ready로 불필요한 일시 정지 제거

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct MyAwaitable {
    bool resultReady = false;
    int cachedResult;
    bool await_ready() const {
        return resultReady;  // 이미 완료됐으면 일시 정지 생략
    }
    void await_suspend(std::coroutine_handle<> h) {
        if (resultReady) return;
        startAsync([this, h]() {
            cachedResult = compute();
            resultReady = true;
            h.resume();
        });
    }
    int await_resume() { return cachedResult; }
};

효과: 이미 완료된 작업에 대해 await_suspend 호출을 피하고, 스케줄링 오버헤드를 줄입니다.

2. 코루틴 프레임 크기 최소화

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 비효율: 큰 지역 변수 → 프레임에 저장됨
Task<void> bad() {
    std::array<char, 1024 * 1024> hugeBuffer;  // 1MB가 프레임에!
    co_await read(socket, hugeBuffer);
}
// ✅ 효율: 필요할 때만 힙에 할당
Task<void> good() {
    auto buf = std::make_unique<std::array<char, 1024>();  // 작은 버퍼
    co_await read(socket, *buf);
}

원리: co_await 이후 재개 시 지역 변수는 코루틴 프레임(힙)에 저장됩니다. 큰 변수는 프레임 크기를 키워 할당/해제 비용을 늘립니다.

3. Allocation 빈도 줄이기

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 매 호출마다 새 Task 생성
Task<int> process(int id) {
    co_return co_await fetch(id);  // fetch가 Task 반환 → 내부적으로 또 할당
}
// ✅ Awaitable만 반환하는 fetch 사용
Task<int> process(int id) {
    co_return co_await fetchAsAwaitable(id);  // 추가 할당 없음
}
연산대략적 비용
코루틴 프레임 할당~100–500ns
handle.resume()~50–100ns
결론: I/O 바운드에서는 네트워크 지연(ms)이 지배적이므로 코루틴 오버헤드(ns)는 무시 가능합니다.

9. 프로덕션 패턴

9.1 HTTP 클라이언트

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

boost::asio::awaitable<std::string> httpGet(
    boost::asio::io_context& io,
    const std::string& host,
    const std::string& path)
{
    boost::asio::ip::tcp::resolver resolver(io);
    auto endpoints = co_await resolver.async_resolve(
        host, "80", boost::asio::use_awaitable);
    boost::asio::ip::tcp::socket socket(io);
    co_await boost::asio::async_connect(socket, endpoints,
        boost::asio::use_awaitable);
    std::string request = "GET " + path + " HTTP/1.1\r\n"
        "Host: " + host + "\r\n\r\n";
    co_await boost::asio::async_write(socket, boost::asio::buffer(request),
        boost::asio::use_awaitable);
    boost::asio::streambuf response;
    co_await boost::asio::async_read_until(socket, response, "\r\n\r\n",
        boost::asio::use_awaitable);
    std::istream is(&response);
    std::string header, body;
    std::getline(is, header);
    std::getline(is, body, '\0');
    co_return body;
}

9.2 DB 쿼리 (개념)

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// DB 커넥션 풀 + 비동기 쿼리
boost::asio::awaitable<User> fetchUser(const std::string& id) {
    auto conn = co_await pool.acquire();
    auto result = co_await conn->async_query(
        "SELECT * FROM users WHERE id = ?", id, boost::asio::use_awaitable);
    User u = parseUser(result);
    co_return u;
}

9.3 타임아웃 래퍼

awaitable_operators&&로 메인 연산과 타이머를 동시에 기다릴 수 있습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
#include <boost/asio/experimental/awaitable_operators.hpp>
boost::asio::awaitable<void> withTimeout(
    boost::asio::awaitable<void> op,
    std::chrono::seconds timeout)
{
    using namespace boost::asio::experimental::awaitable_operators;
    boost::asio::steady_timer timer(co_await boost::asio::this_coro::executor);
    timer.expires_after(timeout);
    // op와 타이머 중 먼저 완료되는 쪽으로 진행
    co_await (std::move(op) || timer.async_wait(boost::asio::use_awaitable));
}

9.4 병렬 co_await (개념)

여러 비동기 작업을 동시에 시작하고 모두 완료를 기다리려면, Asio의 experimental::make_parallel_groupwait_for_all을 사용합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 두 소켓 읽기를 병렬로 수행
// 변수 선언 및 초기화
auto [order, ec1, n1, ec2, n2] = co_await
    boost::asio::experimental::make_parallel_group(
        socket1.async_read_some(boost::asio::buffer(buf1), boost::asio::deferred),
        socket2.async_read_some(boost::asio::buffer(buf2), boost::asio::deferred)
    ).async_wait(
        boost::asio::experimental::wait_for_all(),
        boost::asio::use_awaitable);

자세한 사용법은 Boost.Asio Parallel Group 문서를 참고하세요.

9.5 재시도 + 지수 백오프 (Exponential Backoff)

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

boost::asio::awaitable<std::string> fetchWithBackoff(
    const std::string& url,
    int maxRetries = 5)
{
    auto ex = co_await boost::asio::this_coro::executor;
    std::chrono::milliseconds delay(100);
    for (int i = 0; i < maxRetries; ++i) {
        try {
            co_return co_await httpGet(url);
        } catch (const std::exception& e) {
            if (i == maxRetries - 1) throw;
            std::cerr << "retry " << (i + 1) << "/" << maxRetries
                << " after " << delay.count() << "ms\n";
            boost::asio::steady_timer timer(ex, delay);
            co_await timer.async_wait(boost::asio::use_awaitable);
            delay *= 2;  // 100ms → 200ms → 400ms → ...
        }
    }
    co_return "";
}

9.6 Graceful Shutdown (우아한 종료)

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 전역 플래그로 새 연결 수락 중단
std::atomic<bool> g_running{true};
boost::asio::awaitable<void> listener(
    boost::asio::ip::tcp::acceptor& acceptor)
{
    while (g_running) {
        boost::asio::steady_timer timer(
            co_await boost::asio::this_coro::executor,
            std::chrono::milliseconds(100));
        co_await timer.async_wait(boost::asio::use_awaitable);
        if (!g_running) break;
        boost::system::error_code ec;
        auto socket = co_await acceptor.async_accept(
            boost::asio::redirect_error(boost::asio::use_awaitable, ec));
        if (!ec)
            boost::asio::co_spawn(socket.get_executor(),
                echoSession(std::move(socket)), boost::asio::detached);
    }
}
// SIGINT 핸들러에서 g_running = false 설정 후 io.stop()

9.7 병렬 요청 후 결과 수집

아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 3개 API를 동시에 호출하고 모두 완료될 때까지 대기
boost::asio::awaitable<AggregatedResult> loadDashboard(const std::string& userId) {
    auto [r1, r2, r3] = co_await boost::asio::experimental::make_parallel_group(
        apiClient.getUser(userId),
        apiClient.getOrders(userId),
        apiClient.getNotifications(userId)
    ).async_wait(
        boost::asio::experimental::wait_for_all(),
        boost::asio::use_awaitable);
    co_return AggregatedResult{std::get<0>(r1), std::get<0>(r2), std::get<0>(r3)};
}

9.8 취소(Cancellation) 패턴

Asio의 cancellation_signal을 사용해 장시간 작업을 중단할 수 있습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

boost::asio::awaitable<void> longRunningTask(
    boost::asio::cancellation_signal& cancelSignal)
{
    boost::asio::steady_timer timer(
        co_await boost::asio::this_coro::executor);
    timer.expires_after(std::chrono::seconds(10));
    // 타이머 또는 취소 시그널 중 먼저 발생하는 쪽으로
    boost::system::error_code ec;
    co_await timer.async_wait(
        boost::asio::redirect_error(boost::asio::use_awaitable, ec));
    if (cancelSignal.slot().is_connected()) {
        // 취소 요청이 들어왔으면 조기 종료
    }
}
// 호출 측: cancelSignal.emit(boost::asio::cancellation_type::total);

9.9 로깅·모니터링 패턴

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

boost::asio::awaitable<void> monitoredSession(
    std::string sessionId, boost::asio::ip::tcp::socket socket)
{
    log_info("session started", sessionId);
    std::array<char, 1024> buf;
    try {
        for (;;) {
            auto n = co_await socket.async_read_some(
                boost::asio::buffer(buf), boost::asio::use_awaitable);
            metrics().record_bytes_read(sessionId, n);
            co_await boost::asio::async_write(socket,
                boost::asio::buffer(buf.data(), n), boost::asio::use_awaitable);
        }
    } catch (const std::exception& e) {
        log_error("session error", sessionId, e.what());
        metrics().record_session_error(sessionId);
        throw;
    }
    log_info("session ended", sessionId);
}

프로덕션 권장: 세션 시작/종료/에러 시점 로그, 메트릭(바이트 수, 에러 수) 수집.

9.10 구현 체크리스트

  • Awaitable/코루틴에 값 전달 또는 shared_ptr 사용 (dangling 방지)
  • Task/핸들 수명 관리 (파괴 시 destroy)
  • 재개 스레드 확인 (strand 또는 락)
  • 예외 처리 (unhandled_exception, try/catch)
  • 타임아웃·취소 정책 수립
  • 재시도 + 백오프 (일시적 장애 대응)
  • Graceful shutdown (SIGINT/SIGTERM 처리)
  • 프로파일링으로 성능 확인

참고 자료


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

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


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

C++ 비동기 코루틴, co_await, async 코루틴, awaitable, 콜백 지옥, promise_type, Boost.Asio 코루틴 등으로 검색하시면 이 글이 도움이 됩니다.

정리

항목내용
Awaitableawait_ready, await_suspend(handle), await_resume
Taskpromise_type + operator co_await로 다른 코루틴이 co_await 가능
재개완료 시 handle.resume() 호출
Asiouse_awaitable, co_spawn으로 연동
에러try/catch, unhandled_exception
수명값 복사, shared_ptr, 핸들 관리
실전라이브러리 사용 또는 스케줄러와 연동해 구현

자주 묻는 질문 (FAQ)

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

A. C++20 코루틴으로 비동기 작업을 co_await하고, Task 타입을 설계하며, 이벤트 루프와 연동하는 기본 패턴을 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다. 한 줄 요약: co_await와 Awaitable·Task 설계로 논블로킹 비동기 흐름을 표현할 수 있습니다. 콜백 지옥을 피하고, Asio와 연동해 실전 프로젝트에 적용할 수 있습니다. 다음으로 Modules 기초(#24-1)를 읽어보면 좋습니다. 이전 글: C++ 실전 가이드 #23-2: generator 다음 글: [C++ 실전 가이드 #24-1] C++20 Modules 기초: import로 컴파일 속도 높이기

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3