[2026] C++20 Coroutines 완벽 가이드 | 비동기 프로그래밍의 새 시대

[2026] C++20 Coroutines 완벽 가이드 | 비동기 프로그래밍의 새 시대

이 글의 핵심

C++20 Coroutines : 비동기 프로그래밍의 새 시대. C++20 Coroutines란?. 왜 필요한가·기본 키워드.

🎯 이 글을 읽으면 (읽는 시간: 22분)

TL;DR: C++20 코루틴으로 콜백 지옥을 탈출합니다. co_await, co_return, co_yield를 사용해 비동기 코드를 동기 코드처럼 깔끔하게 작성하는 방법을 배웁니다. 이 글을 읽으면:

  • ✅ C++20 코루틴의 3가지 키워드 (co_await, co_return, co_yield) 완벽 이해
  • ✅ 콜백 지옥 없이 우아한 비동기 코드 작성
  • ✅ Generator 패턴으로 지연 평가 구현 실무 활용:
  • 🔥 비동기 I/O 처리 (파일, 네트워크)
  • 🔥 게임 엔진 (프레임 단위 실행)
  • 🔥 대용량 데이터 스트리밍 난이도: 고급 | 실습 코드: 10개 | C++20 필수

C++20 Coroutines란? 왜 필요한가

문제 시나리오: 콜백 지옥

문제: 비동기 작업을 콜백으로 처리하면 코드가 중첩되어 가독성이 떨어집니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 콜백 지옥
async_read_file("config.json",  {
    auto config = parse_json(content);
    async_fetch_url(config.url,  {
        auto data = parse_response(response);
        async_save_db(data,  {
            if (success) {
                std::cout << "Done\n";
            }
        });
    });
});

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

// Coroutine으로 깔끔하게
Task<void> process() {
    auto content = co_await async_read_file("config.json");
    auto config = parse_json(content);
    auto response = co_await async_fetch_url(config.url);
    auto data = parse_response(response);
    bool success = co_await async_save_db(data);
    if (success) {
        std::cout << "Done\n";
    }
}

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

// 실행 예제
flowchart TD
    subgraph callback[콜백 방식]
        c1[async_read_file(callback1)]
        c2["callback1: parse_json"]
        c3[async_fetch_url(callback2)]
        c4["callback2: parse_response"]
        c5[async_save_db(callback3)]
    end
    subgraph coroutine[Coroutine 방식]
        co1["co_await async_read_file"]
        co2[parse_json]
        co3["co_await async_fetch_url"]
        co4[parse_response]
        co5["co_await async_save_db"]
    end
    c1 --> c2 --> c3 --> c4 --> c5
    co1 --> co2 --> co3 --> co4 --> co5

목차

  1. 기본 키워드: co_await, co_yield, co_return
  2. Promise Type
  3. Generator 구현
  4. Task 구현 (비동기)
  5. Awaitable 객체
  6. 자주 발생하는 문제와 해결법
  7. 프로덕션 패턴
  8. 완전한 예제: 비동기 HTTP 클라이언트
  9. 성능 고려사항

1. 기본 키워드

co_yield: 값 반환 후 중단

아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Generator<int> counter(int max) {
    for (int i = 0; i < max; ++i) {
        co_yield i;  // i를 반환하고 중단
    }
}
int main() {
    auto gen = counter(5);
    while (gen.next()) {
        std::cout << gen.value() << '\n';
    }
    // 0 1 2 3 4
}

co_return: 최종 값 반환 후 종료

다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

Task<int> compute() {
    int result = 42;
    co_return result;  // 종료
}

co_await: 비동기 작업 대기

다음은 간단한 cpp 코드 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

Task<std::string> fetch_data() {
    auto response = co_await async_http_get("https://api.example.com/data");
    co_return response;
}

2. Promise Type

Promise Type이란

Coroutine의 동작을 정의하는 타입입니다. Coroutine 함수의 반환 타입에 promise_type이 중첩되어 있어야 합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

struct MyCoroutine {
    struct promise_type {
        // 1. Coroutine 객체 생성
        MyCoroutine get_return_object() {
            return MyCoroutine{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        
        // 2. 초기 중단 여부
        std::suspend_always initial_suspend() { return {}; }  // 중단
        // std::suspend_never initial_suspend() { return {}; }  // 즉시 실행
        
        // 3. 최종 중단 여부
        std::suspend_always final_suspend() noexcept { return {}; }
        
        // 4. 반환 처리
        void return_void() {}
        // void return_value(T value) { this->value = value; }
        
        // 5. 예외 처리
        void unhandled_exception() {
            exception = std::current_exception();
        }
        
        // 6. yield 처리 (Generator용)
        std::suspend_always yield_value(T value) {
            this->value = value;
            return {};
        }
        
        T value;
        std::exception_ptr exception;
    };
    
    std::coroutine_handle<promise_type> handle;
    
    ~MyCoroutine() {
        if (handle) handle.destroy();
    }
};

3. Generator 구현

완전한 Generator

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

#include <coroutine>
#include <iostream>
#include <stdexcept>
template<typename T>
struct Generator {
    struct promise_type {
        T value;
        std::exception_ptr exception;
        
        Generator get_return_object() {
            return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        
        void return_void() {}
        
        void unhandled_exception() {
            exception = std::current_exception();
        }
        
        std::suspend_always yield_value(T v) {
            value = v;
            return {};
        }
    };
    
    std::coroutine_handle<promise_type> handle;
    
    explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
    
    ~Generator() {
        if (handle) handle.destroy();
    }
    
    // 복사 금지
    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;
    
    // 이동 가능
    Generator(Generator&& other) noexcept : handle(other.handle) {
        other.handle = nullptr;
    }
    
    Generator& operator=(Generator&& other) noexcept {
        if (this != &other) {
            if (handle) handle.destroy();
            handle = other.handle;
            other.handle = nullptr;
        }
        return *this;
    }
    
    bool next() {
        if (!handle || handle.done()) return false;
        handle.resume();
        if (handle.promise().exception) {
            std::rethrow_exception(handle.promise().exception);
        }
        return !handle.done();
    }
    
    T value() const {
        return handle.promise().value;
    }
};
// 사용 예시: 피보나치
Generator<int> fibonacci(int n) {
    int a = 0, b = 1;
    for (int i = 0; i < n; ++i) {
        co_yield a;
        int next = a + b;
        a = b;
        b = next;
    }
}
int main() {
    auto fib = fibonacci(10);
    while (fib.next()) {
        std::cout << fib.value() << ' ';
    }
    std::cout << '\n';
    // 0 1 1 2 3 5 8 13 21 34
}

4. Task 구현 (비동기)

완전한 Task

#include <coroutine>
#include <exception>
#include <iostream>
template<typename T>
struct Task {
    struct promise_type {
        T value;
        std::exception_ptr exception;
        
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        
        std::suspend_never initial_suspend() { return {}; }  // 즉시 실행
        std::suspend_always final_suspend() noexcept { return {}; }
        
        void return_value(T v) {
            value = v;
        }
        
        void unhandled_exception() {
            exception = std::current_exception();
        }
    };
    
    std::coroutine_handle<promise_type> handle;
    
    explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
    
    ~Task() {
        if (handle) handle.destroy();
    }
    
    Task(const Task&) = delete;
    Task& operator=(const Task&) = delete;
    
    Task(Task&& other) noexcept : handle(other.handle) {
        other.handle = nullptr;
    }
    
    T get() {
        if (!handle.done()) {
            handle.resume();
        }
        if (handle.promise().exception) {
            std::rethrow_exception(handle.promise().exception);
        }
        return handle.promise().value;
    }
    
    bool done() const {
        return handle.done();
    }
};
// 사용 예시
Task<int> async_compute(int x) {
    // 비동기 작업 시뮬레이션
    co_return x * x;
}
int main() {
    auto task = async_compute(10);
    std::cout << "Result: " << task.get() << '\n';  // 100
}

5. Awaitable 객체

Awaitable 인터페이스

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

struct Awaitable {
    // 1. 즉시 완료 여부
    bool await_ready() const noexcept {
        return false;  // false면 중단
    }
    
    // 2. 중단 시 실행
    void await_suspend(std::coroutine_handle<> h) const noexcept {
        // 중단 로직 (스레드 풀에 작업 추가 등)
    }
    
    // 3. 재개 시 반환값
    int await_resume() const noexcept {
        return 42;
    }
};
Task<int> example() {
    int value = co_await Awaitable{};
    co_return value;
}

실전 Awaitable: 타이머

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

#include <coroutine>
#include <chrono>
#include <thread>
struct SleepAwaitable {
    std::chrono::milliseconds duration;
    
    bool await_ready() const noexcept {
        return duration.count() <= 0;
    }
    
    void await_suspend(std::coroutine_handle<> h) const {
        std::thread([h, d = duration]() {
            std::this_thread::sleep_for(d);
            h.resume();
        }).detach();
    }
    
    void await_resume() const noexcept {}
};
Task<void> delayed_print() {
    std::cout << "Start\n";
    co_await SleepAwaitable{std::chrono::seconds(1)};
    std::cout << "After 1 second\n";
    co_await SleepAwaitable{std::chrono::seconds(2)};
    std::cout << "After 3 seconds total\n";
}

6. 자주 발생하는 문제와 해결법

문제 1: promise_type 누락

증상: error: unable to find the promise type for this coroutine. 원인: 반환 타입에 promise_type이 없음. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 사용
struct MyCoroutine {
    // promise_type 없음
};
MyCoroutine func() {
    co_return;  // Error
}
// ✅ 올바른 사용
struct MyCoroutine {
    struct promise_type {
        MyCoroutine get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

문제 2: 수명 관리

증상: 크래시, dangling handle. 원인: Coroutine handle을 제대로 파괴하지 않음. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 사용
struct Generator {
    std::coroutine_handle<promise_type> handle;
    // 소멸자 없음 → 메모리 누수
};
// ✅ 올바른 사용: RAII
struct Generator {
    std::coroutine_handle<promise_type> handle;
    
    ~Generator() {
        if (handle) handle.destroy();
    }
    
    // 복사 금지
    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;
    
    // 이동 가능
    Generator(Generator&& other) noexcept : handle(other.handle) {
        other.handle = nullptr;
    }
};

문제 3: 예외 처리

증상: Coroutine 내부 예외가 무시됨. 다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 사용
struct promise_type {
    void unhandled_exception() {
        // 아무것도 안 함 → 예외 손실
    }
};
// ✅ 올바른 사용: 예외 저장 후 재발생
struct promise_type {
    std::exception_ptr exception;
    
    void unhandled_exception() {
        exception = std::current_exception();
    }
};
T get() {
    if (handle.promise().exception) {
        std::rethrow_exception(handle.promise().exception);
    }
    return handle.promise().value;
}

문제 4: co_await와 일반 함수 혼용

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

// ❌ 잘못된 사용
void regular_function() {
    co_await something();  // Error: not a coroutine
}
// ✅ 올바른 사용: Coroutine 함수
Task<void> coroutine_function() {
    co_await something();  // OK
}

7. 프로덕션 패턴

패턴 1: 에러 처리

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

template<typename T>
struct Result {
    std::variant<T, std::string> data;
    
    bool has_value() const {
        return std::holds_alternative<T>(data);
    }
    
    T value() const {
        return std::get<T>(data);
    }
    
    std::string error() const {
        return std::get<std::string>(data);
    }
};
Task<Result<std::string>> safe_fetch(const std::string& url) {
    try {
        auto response = co_await async_http_get(url);
        co_return Result<std::string>{response};
    } catch (const std::exception& e) {
        co_return Result<std::string>{std::string(e.what())};
    }
}

패턴 2: Generator 체이닝

다음은 cpp를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

Generator<int> map(Generator<int> gen, int (*f)(int)) {
    while (gen.next()) {
        co_yield f(gen.value());
    }
}
Generator<int> filter(Generator<int> gen, bool (*pred)(int)) {
    while (gen.next()) {
        int val = gen.value();
        if (pred(val)) {
            co_yield val;
        }
    }
}
Generator<int> range(int start, int end) {
    for (int i = start; i < end; ++i) {
        co_yield i;
    }
}
int main() {
    auto gen = range(0, 10);
    auto doubled = map(std::move(gen),  { return x * 2; });
    auto evens = filter(std::move(doubled),  { return x % 2 == 0; });
    
    while (evens.next()) {
        std::cout << evens.value() << ' ';
    }
    // 0 4 8 12 16
}

패턴 3: 비동기 타임아웃

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

template<typename T>
Task<std::optional<T>> with_timeout(Task<T> task, std::chrono::milliseconds timeout) {
    auto start = std::chrono::steady_clock::now();
    
    while (!task.done()) {
        auto elapsed = std::chrono::steady_clock::now() - start;
        if (elapsed > timeout) {
            co_return std::nullopt;  // 타임아웃
        }
        co_await std::suspend_always{};
    }
    
    co_return task.get();
}

8. 완전한 예제: 비동기 파일 처리

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

#include <coroutine>
#include <fstream>
#include <string>
#include <iostream>
template<typename T>
struct Task {
    struct promise_type {
        T value;
        std::exception_ptr exception;
        
        Task get_return_object() {
            return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
        }
        
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        
        void return_value(T v) { value = v; }
        void unhandled_exception() { exception = std::current_exception(); }
    };
    
    std::coroutine_handle<promise_type> handle;
    
    explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
    ~Task() { if (handle) handle.destroy(); }
    
    Task(const Task&) = delete;
    Task(Task&& other) noexcept : handle(other.handle) {
        other.handle = nullptr;
    }
    
    T get() {
        if (!handle.done()) handle.resume();
        if (handle.promise().exception) {
            std::rethrow_exception(handle.promise().exception);
        }
        return handle.promise().value;
    }
};
// 비동기 파일 읽기 (시뮬레이션)
Task<std::string> async_read_file(const std::string& path) {
    std::ifstream file(path);
    if (!file) {
        throw std::runtime_error("File not found: " + path);
    }
    
    std::string content((std::istreambuf_iterator<char>(file)),
                        std::istreambuf_iterator<char>());
    co_return content;
}
// 파일 처리 파이프라인
Task<int> process_files() {
    try {
        auto content1 = co_await async_read_file("file1.txt");
        std::cout << "File1 size: " << content1.size() << '\n';
        
        auto content2 = co_await async_read_file("file2.txt");
        std::cout << "File2 size: " << content2.size() << '\n';
        
        co_return content1.size() + content2.size();
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << '\n';
        co_return -1;
    }
}
int main() {
    auto task = process_files();
    int total_size = task.get();
    std::cout << "Total: " << total_size << '\n';
}

9. 성능 고려사항

Coroutine vs 스레드

항목Coroutine스레드
생성 비용낮음 (수백 바이트)높음 (수 MB)
컨텍스트 스위칭빠름 (함수 호출 수준)느림 (커널 개입)
동시 실행협력적 (명시적 중단)선점적 (OS 스케줄링)
적합 용도I/O 대기, GeneratorCPU 집약 병렬 작업
요약: I/O 대기가 많은 비동기 작업은 Coroutine이 스레드보다 효율적입니다. 수천 개의 동시 연결을 처리하는 서버에서 유용합니다.

정리

개념설명
Coroutine중단/재개 가능한 함수
co_yield값 반환 후 중단
co_return최종 값 반환 후 종료
co_await비동기 작업 대기
Promise TypeCoroutine 동작 정의
Generator지연 평가 시퀀스
Task비동기 작업
C++20 Coroutines는 비동기 코드를 동기식으로 작성할 수 있게 해, 가독성과 유지보수성을 크게 향상시킵니다.

FAQ

Q1: Coroutine vs async/await (다른 언어)?

A: C++의 Coroutine은 저수준 메커니즘입니다. Promise Type을 직접 구현해야 하지만, 그만큼 유연합니다. C#/JavaScript의 async/await는 언어에 내장된 고수준 기능입니다.

Q2: Generator vs std::ranges?

A: Generator지연 평가로 값을 하나씩 생성합니다. std::ranges는 기존 컨테이너를 뷰로 보는 것이고, Generator는 값을 동적으로 생성합니다.

Q3: co_await는 항상 중단하나요?

A: await_ready()true를 반환하면 중단하지 않고 즉시 await_resume()을 호출합니다. 이미 완료된 작업은 중단 없이 진행할 수 있습니다.

Q4: Coroutine은 스레드를 만드나요?

A: 아니요. Coroutine 자체는 단일 스레드에서 실행됩니다. await_suspend에서 스레드 풀에 작업을 넘기는 식으로 멀티스레드와 조합할 수 있습니다.

Q5: 컴파일러 지원은?

A:

  • GCC 10+: 완전 지원 (-fcoroutines)
  • Clang 14+: 완전 지원
  • MSVC 2019+: 완전 지원 (/await)

Q6: Coroutines 학습 리소스는?

A:


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

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

관련 글

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