본문으로 건너뛰기
Previous
Next
C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기

C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기

C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기

이 글의 핵심

C++ 메모리 부족·OOM 해결, Generator 완전 구현(co_yield·promise_type), lazy 평가·무한 시퀀스, filter·map·take 조합, 자주 발생하는 에러(dangling·lifetime)·해결법, 성능 비교, 파일 읽기·데이터 스트리밍 프로덕션 패턴.

들어가며: “시퀀스를 만들다 메모리 부족 에러가 나요”

실제 겪는 문제 시나리오

// ❌ 문제: 대용량 시퀀스가 필요한데...
// - 10GB 로그 파일에서 ERROR 라인만 앞 100개 찾기
// - API 페이지네이션: 100만 건 중 처음 20개만 표시
// - 피보나치·소수 같은 무한 수열 — 벡터로는 표현 불가
// vector에 전부 담으면 → OOM(Out of Memory)
// 전체를 미리 계산하면 → 불필요한 CPU·메모리 낭비

시나리오 1 (로그 분석): 10GB 로그에서 ERROR 라인만 앞 100개 추출. vector<string>에 전체를 올리면 RAM 부족으로 크래시. 필요한 만큼만 스트리밍으로 읽어야 합니다. 시나리오 2 (API 페이지네이션): DB에 100만 건. 처음 20개만 화면에 표시하는데, 전부 fetch하면 네트워크·메모리 낭비. Generator로 “요청할 때마다 다음 N개”만 가져오는 패턴이 적합합니다. 시나리오 3 (무한 수열): 피보나치, 소수, 카운터처럼 끝이 없는 시퀀스. 벡터는 크기를 미리 알아야 하므로 표현 불가. Generator는 co_yield로 “다음 값 하나”만 내보내므로 무한 시퀀스를 자연스럽게 표현합니다. 시나리오 4 (데이터 파이프라인): filter → map → take처럼 여러 단계를 거치는 처리. 중간 결과를 전부 벡터에 담으면 메모리 폭발. Generator 파이프라인은 값이 흐르는 대로 한 개씩 처리합니다. 실제 프로덕션에서 겪는 문제들:

  • OOM: 대용량 시퀀스를 vector에 담다 메모리 부족
  • 초기 지연: 전체 생성이 끝날 때까지 블로킹
  • 불필요한 계산: 앞 10개만 쓰는데 100만 개 전부 계산
  • 무한 시퀀스 표현 불가: 벡터는 크기 한계가 있음 해결책: Generatorco_yield필요할 때마다 값 하나씩 만들어 주는 lazy 시퀀스. 100만 개를 미리 만들지 않고, next() 호출 시마다 다음 값만 계산합니다.

문제 상황 (요약)

0부터 100만까지 정수를 만들어야 하는데, 앞 10개만 쓰고 끝내는 경우가 있습니다. 전통적인 방식은 std::vector<int>에 100만 개를 전부 채워 넣는 것입니다. 그러면 약 4MB 메모리가 한 번에 할당되고, 99만 9,990개는 결국 버려집니다. 시퀀스가 더 크거나 요소 타입이 더 무거우면 메모리 부족이나 지연이 심해집니다. 제너레이터(Generator)필요할 때마다 값 하나씩 만들어 주는 lazy 시퀀스입니다. 100만 개를 미리 만들지 않고, next()를 호출할 때마다 다음 값만 계산합니다. 10개만 쓰고 멈추면 나머지는 계산하지 않으므로 메모리와 CPU를 절약할 수 있습니다. 목표:

  • 문제 시나리오: 메모리를 많이 쓰는 시퀀스 생성
  • Generator 완전 구현: promise_type, iterator, 범위 기반 for
  • 무한 제너레이터: 끝이 없는 시퀀스
  • 제너레이터 조합: filter, map, 파이프라인
  • 자주 발생하는 오류: lifetime(수명) 이슈
  • 성능 비교: generator vs vector
  • 프로덕션 패턴: 파일 읽기, 데이터 스트리밍 이 글을 읽으면:
  • co_yield로 lazy 시퀀스를 만드는 방법을 알 수 있습니다.
  • Generator 타입을 처음부터 구현할 수 있습니다.
  • 무한 시퀀스와 조합 패턴을 활용할 수 있습니다.
  • lifetime 관련 실수를 피할 수 있습니다.
  • 실무에서 파일·스트리밍 처리에 적용할 수 있습니다. 요구 환경: C++20 (g++ -std=c++20 또는 clang++ -std=c++20). C++23에서는 std::generator를 사용할 수 있습니다.

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다. 일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.

1. 문제 시나리오: 메모리를 많이 쓰는 시퀀스 생성

벡터 방식의 한계

대용량 시퀀스를 만들 때 std::vector를 쓰면 전체를 한 번에 메모리에 올립니다.

// ❌ 100만 개 전부 할당 — 앞 10개만 쓰는데 4MB 사용
std::vector<int> createSequence(int n) {
    std::vector<int> vec;
    vec.reserve(n);
    for (int i = 0; i < n; ++i) {
        vec.push_back(i);
    }
    return vec;
}
int main() {
    auto seq = createSequence(1'000'000);
    for (int i = 0; i < 10; ++i) {  // 10개만 사용
        process(seq[i]);
    }
    // 99만 9,990개는 메모리에 그대로 — 낭비
}

문제점:

  • 메모리: n이 크면 O(n) 메모리 사용
  • 초기 지연: 전체 시퀀스 생성이 끝날 때까지 대기
  • 불필요한 계산: 앞부분만 쓰면 나머지 계산이 낭비

Generator로 해결

Generator는 호출할 때마다 다음 값 하나만 만들어 줍니다. 필요한 만큼만 계산하고, 나머지는 생성하지 않습니다.

// ✅ Generator: 필요한 만큼만 계산
Generator<int> count(int n) {
    for (int i = 0; i < n; ++i) {
        co_yield i;  // i를 넘기고 일시 정지
    }
}
int main() {
    auto gen = count(1'000'000);
    for (int i = 0; i < 10; ++i) {
        auto opt = gen.next();  // 10번만 계산
        if (opt) process(*opt);
    }
    // 나머지 99만 9,990개는 계산하지 않음
}

장점:

  • 메모리: O(1) — 현재 값 하나만 유지
  • 지연: 첫 값은 즉시, 이후 값은 요청 시마다
  • lazy: 필요한 만큼만 계산

시각화

flowchart TB
    subgraph vector[벡터 방식]
        V1[createSequence 호출] --> V2[100만 개 전부 할당]
        V2 --> V3[메모리 ~4MB]
        V3 --> V4[10개만 사용]
        V4 --> V5[99만 9,990개 낭비]
    end
    subgraph generator[Generator 방식]
        G1[count 호출] --> G2[첫 co_yield까지 대기]
        G2 --> G3[next 10번 호출]
        G3 --> G4[10개만 계산]
        G4 --> G5[나머지 미계산]
    end

추가 활용 사례

  • 트리/그래프 DFS: 방문한 노드만 co_yield — 전체 경로 저장 불필요
  • 조건부 조기 종료: primes()에서 p > n인 첫 소수 찾을 때까지만 계산
  • 임베디드: 1MB RAM 장치에서 vector 할당 실패 시 Generator로 스트리밍
  • 실시간 스트리밍: 센서·이벤트 로그처럼 끝이 없는 스트림 (비동기는 co_await와 결합)

2. Lazy 평가 (지연 평가) 이해하기

Lazy vs Eager

Eager(즉시 평가): 시퀀스를 한 번에 전부 계산합니다. vectorpush_back으로 채우는 방식이 여기에 해당합니다. Lazy(지연 평가): 값이 필요할 때만 계산합니다. co_yield는 “이 값을 넘기고 일시 정지”하므로, 호출자가 다음 값을 요청할 때까지 다음 계산이 미뤄집니다.

// Eager: 100만 개 전부 계산 후 반환
std::vector<int> eager = createSequence(1'000'000);  // 즉시 100만 개 생성
// Lazy: 10개만 요청하면 10개만 계산
auto gen = count(1'000'000);
for (int i = 0; i < 10; ++i) {
    auto v = gen.next();  // i번째 요청 시에만 i번째 값 계산
}

Lazy 평가 흐름

sequenceDiagram
    participant C as 호출자
    participant G as Generator
    C->>G: next() 1회
    G->>G: co_yield 0
    G-->>C: 0 반환, 일시 정지
    C->>C: 0 사용
    C->>G: next() 2회
    G->>G: co_yield 1
    G-->>C: 1 반환
    Note over G: 2, 3, 4....는 아직 계산 안 함

핵심: count(1'000'000)을 호출해도 100만 개를 만들지 않습니다. next()를 10번 호출하면 10개만 계산하고, 나머지는 생성되지 않습니다.

Lazy의 장점

항목Eager (vector)Lazy (Generator)
메모리O(n)O(1)
초기 지연전체 생성 완료까지첫 값만
조기 종료나머지도 이미 계산됨요청 안 하면 미계산
무한 시퀀스불가가능

3. Generator 완전 구현

promise_type 설계

Generator의 동작은 promise_type으로 제어합니다. 이전 글(#23-1)에서 다룬 코루틴 기초를 바탕으로, co_yield에 맞는 promise를 정의합니다.

#include <coroutine>
#include <exception>
#include <optional>
template <typename T>
class Generator {
public:
    struct promise_type {
        T current_value;
        std::exception_ptr exception;
        // co_yield value 호출 시: 값을 저장하고 일시 정지
        std::suspend_always yield_value(T value) {
            current_value = std::move(value);
            return {};
        }
        // 코루틴 진입 직후 일시 정지 — 첫 next()에서 resume
        std::suspend_always initial_suspend() { return {}; }
        // co_return 후 일시 정지 — 호출자가 done() 확인 가능
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { exception = std::current_exception(); }
        Generator get_return_object() {
            return Generator{
                std::coroutine_handle<promise_type>::from_promise(*this)
            };
        }
    };
    // ....(아래 계속)
};

핵심 포인트:

  • yield_value(T): co_yield 값 저장 후 suspend_always로 일시 정지
  • initial_suspend() = suspend_always: 첫 resume() 전까지 대기
  • final_suspend() = suspend_always: 종료 후 done() 확인 가능

iterator와 범위 기반 for

begin()/end()를 제공하면 범위 기반 for로 사용할 수 있습니다.

    struct iterator {
        std::coroutine_handle<promise_type> handle;
        bool done;
        iterator(std::coroutine_handle<promise_type> h, bool d)
            : handle(h), done(d) {}
        T operator*() const {
            return handle.promise().current_value;
        }
        iterator& operator++() {
            handle.resume();
            if (handle.promise().exception)
                std::rethrow_exception(handle.promise().exception);
            done = handle.done();
            return *this;
        }
        bool operator!=(const iterator& other) const {
            return done != other.done;
        }
    };
    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;
    }
    iterator begin() {
        handle_.resume();
        if (handle_.promise().exception)
            std::rethrow_exception(handle_.promise().exception);
        return iterator{handle_, handle_.done()};
    }
    iterator end() { return iterator{handle_, true}; }
private:
    std::coroutine_handle<promise_type> handle_;
};

next() 인터페이스 (선택)

next()로 값을 하나씩 꺼내는 인터페이스도 추가할 수 있습니다.

    std::optional<T> next() {
        if (!handle_ || handle_.done()) return std::nullopt;
        handle_.resume();
        if (handle_.done()) return std::nullopt;
        return handle_.promise().current_value;
    }

완전한 예제

// g++ -std=c++20 -o generator_basic generator_basic.cpp && ./generator_basic
#include <coroutine>
#include <exception>
#include <iostream>
#include <optional>
template <typename T>
class Generator {
public:
    struct promise_type {
        T current_value;
        std::exception_ptr exception;
        std::suspend_always yield_value(T value) {
            current_value = std::move(value);
            return {};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { exception = std::current_exception(); }
        Generator get_return_object() {
            return Generator{
                std::coroutine_handle<promise_type>::from_promise(*this)};
        }
    };
    struct iterator {
        std::coroutine_handle<promise_type> handle;
        bool done;
        iterator(std::coroutine_handle<promise_type> h, bool d)
            : handle(h), done(d) {}
        T operator*() const { return handle.promise().current_value; }
        iterator& operator++() {
            handle.resume();
            if (handle.promise().exception)
                std::rethrow_exception(handle.promise().exception);
            done = handle.done();
            return *this;
        }
        bool operator!=(const iterator& other) const {
            return done != other.done;
        }
    };
    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;
    }
    iterator begin() {
        handle_.resume();
        if (handle_.promise().exception)
            std::rethrow_exception(handle_.promise().exception);
        return iterator{handle_, handle_.done()};
    }
    iterator end() { return iterator{handle_, true}; }
    std::optional<T> next() {
        if (!handle_ || handle_.done()) return std::nullopt;
        handle_.resume();
        if (handle_.done()) return std::nullopt;
        return handle_.promise().current_value;
    }
private:
    std::coroutine_handle<promise_type> handle_;
};
Generator<int> count(int n) {
    for (int i = 0; i < n; ++i) {
        co_yield i;
    }
}
int main() {
    for (int x : count(5)) {
        std::cout << x << " ";  // 0 1 2 3 4
    }
    std::cout << "\n";
}

실행 결과: 0 1 2 3 4

co_yield 흐름 다이어그램

sequenceDiagram
    participant C as 호출자
    participant G as Generator
    participant P as promise_type
    participant Co as 코루틴
    C->>G: begin()
    G->>Co: handle_.resume()
    Co->>P: yield_value(0)
    P-->>Co: suspend_always
    Co-->>G: 일시 정지
    G-->>C: iterator(0 반환)
    C->>C: *it → 0
    C->>G: ++it
    G->>Co: handle_.resume()
    Co->>P: yield_value(1)
    Note over Co: ....반복 ...

4. 무한 제너레이터

끝이 없는 시퀀스

Generator는 끝이 없는 시퀀스도 표현할 수 있습니다. co_return이 없으면 이론적으로 무한히 co_yield할 수 있고, 호출하는 쪽에서 필요한 만큼만 꺼내 쓰면 됩니다.

// 무한 피보나치 수열
// 실행 예제
Generator<long long> fibonacci() {
    long long a = 0, b = 1;
    while (true) {
        co_yield a;
        auto next = a + b;
        a = b;
        b = next;
    }
}
int main() {
    int count = 0;
    for (long long x : fibonacci()) {
        std::cout << x << " ";
        if (++count >= 15) break;  // 15개만 출력
    }
    // 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377
}

무한 카운터

Generator<int> counter(int start = 0) {
    int i = start;
    while (true) {
        co_yield i++;
    }
}
// 사용: zip 등에 활용
auto gen = counter(10);
auto it = gen.begin();
for (int i = 0; i < 5; ++i) {
    std::cout << *it << " ";
    ++it;
}
// 10 11 12 13 14

소수 생성기

Generator<int> primes() {
    co_yield 2;
    for (int n = 3; ; n += 2) {
        bool is_prime = true;
        for (int d = 3; d * d <= n; d += 2) {
            if (n % d == 0) { is_prime = false; break; }
        }
        if (is_prime) co_yield n;
    }
}
// 처음 10개 소수
int n = 0;
for (int p : primes()) {
    std::cout << p << " ";
    if (++n >= 10) break;
}
// 2 3 5 7 11 13 17 19 23 29

주의사항

무한 Generator를 범위 기반 for에 그대로 넣으면 무한 루프가 됩니다. 반드시 break나 개수 제한을 두어야 합니다.

// ❌ 위험: 무한 루프
for (auto x : fibonacci()) {
    process(x);  // 절대 끝나지 않음
}
// ✅ 안전: 개수 제한
int n = 0;
for (auto x : fibonacci()) {
    process(x);
    if (++n >= 100) break;
}

5. 제너레이터 조합

filter: 조건에 맞는 값만

다른 Generator를 받아 조건을 만족하는 값만 co_yield하는 Generator를 만들 수 있습니다.

template <typename T, typename Pred>
Generator<T> filter(Generator<T> src, Pred pred) {
    for (T x : src) {
        if (pred(x)) co_yield x;
    }
}
// 사용: 짝수만
auto nums = count(20);
auto evens = filter(std::move(nums),  { return x % 2 == 0; });
for (int x : evens) {
    std::cout << x << " ";  // 0 2 4 6 8 10 12 14 16 18
}

map: 값 변환

template <typename T, typename F>
Generator<std::invoke_result_t<F, T>> map(Generator<T> src, F f) {
    for (T x : src) {
        co_yield f(x);
    }
}
// 사용: 제곱
auto nums = count(5);
auto squares = map(std::move(nums),  { return x * x; });
for (int x : squares) {
    std::cout << x << " ";  // 0 1 4 9 16
}

파이프라인

filter와 map을 연결해 파이프라인을 구성할 수 있습니다.

// count(100) → 짝수만 → 제곱 → 10개만
auto nums = count(100);
auto evens = filter(std::move(nums),  { return x % 2 == 0; });
auto squares = map(std::move(evens),  { return x * x; });
int n = 0;
for (int x : squares) {
    std::cout << x << " ";
    if (++n >= 10) break;
}
// 0 4 16 36 64 100 144 196 256 324

take: 상위 N개만

template <typename T>
Generator<T> take(Generator<T> src, size_t n) {
    size_t count = 0;
    for (T x : src) {
        if (count++ >= n) break;
        co_yield x;
    }
}
// 사용
for (int x : take(count(1000), 5)) {
    std::cout << x << " ";  // 0 1 2 3 4
}

zip: 두 시퀀스 병합

template <typename T, typename U>
Generator<std::pair<T, U>> zip(Generator<T> a, Generator<U> b) {
    auto it_a = a.begin();
    auto it_b = b.begin();
    auto end_a = a.end();
    auto end_b = b.end();
    while (it_a != end_a && it_b != end_b) {
        co_yield {*it_a, *it_b};
        ++it_a;
        ++it_b;
    }
}
// 사용
auto nums = count(5);
auto letters = /* Generator<char> 예시 */;
for (auto [n, c] : zip(std::move(nums), std::move(letters))) {
    std::cout << "(" << n << "," << c << ") ";
}

enumerate, sliding_window, flatten, distinct

  • enumerate: {idx++, x}로 인덱스와 값 쌍 반환 (Python enumerate()와 동일)
  • sliding_window: 연속 N개 구간 — 이동 평균, 패턴 탐지에 활용
  • flatten: Generator<Generator<T>>Generator<T> 평탄화
  • distinct: std::unordered_set으로 중복 제거
template <typename T>
Generator<std::pair<size_t, T>> enumerate(Generator<T> src) {
    size_t idx = 0;
    for (T x : src) co_yield {idx++, x};
}
template <typename T>
Generator<T> distinct(Generator<T> src) {
    std::unordered_set<T> seen;
    for (T x : src)
        if (seen.insert(x).second) co_yield x;
}

6. 자주 발생하는 오류 (lifetime 수명)

1. Dangling 참조 (죽은 참조)

원인: co_yield참조를 반환하고, 그 참조가 가리키는 객체가 코루틴 재개 전에 소멸하는 경우.

// ❌ 위험: string_view는 참조 — 임시 string 소멸 후 접근
Generator<std::string_view> splitBad(std::string_view s) {
    size_t pos = 0;
    while (pos < s.size()) {
        auto end = s.find(' ', pos);
        if (end == std::string_view::npos) end = s.size();
        co_yield s.substr(pos, end - pos);  // UB 가능
        pos = end == s.size() ? end : end + 1;
    }
}
// 호출 시
for (auto part : splitBad(std::string("a b c"))) {  // 임시 string
    // part가 이미 소멸한 임시를 참조 — UB
}
// ✅ 안전: 값으로 복사
Generator<std::string> splitGood(std::string s) {
    size_t pos = 0;
    while (pos < s.size()) {
        auto end = s.find(' ', pos);
        if (end == std::string_view::npos) end = s.size();
        co_yield s.substr(pos, end - pos);
        pos = end == s.size() ? end : end + 1;
    }
}

2. Generator 이동 후 사용

원인: Generator는 이동 전용입니다. filtermap에 넘긴 뒤 원본을 다시 쓰면 안 됩니다.

auto nums = count(10);
auto evens = filter(std::move(nums),  { return x % 2 == 0; });
// nums는 이미 비어 있음 (handle이 이동됨)
for (int x : nums) { }   // ❌ UB: 빈 Generator
for (int x : evens) { }  // ✅ OK

3. 핸들 수명 관리

원인: coroutine_handle만 복사해 두고, Generator 객체는 소멸시킨 뒤 핸들로 resume 호출.

// ❌ 위험
std::coroutine_handle<> dangerous;
{
    auto gen = count(5);
    dangerous = gen.handle();  // 핸들만 복사 (실제로는 handle_이 private)
}  // gen 소멸 → destroy() 호출
dangerous.resume();  // UB: 이미 해제된 프레임
// ✅ 안전: Generator 객체가 살아 있는 동안만 사용
auto gen = count(5);
for (int x : gen) { /* ....*/ }

4. iterator 무효화

원인: begin()으로 얻은 iterator를 사용하는 동안 Generator를 이동하거나 소멸하면 iterator가 무효화됩니다.

auto gen = count(5);
auto it = gen.begin();
gen = count(10);  // gen 이동 — it가 가리키는 handle 무효
++it;  // ❌ UB

5. 지역 변수 참조 co_yield

원인: 스택에 있는 지역 변수의 주소나 참조를 co_yield하면, 코루틴이 일시 정지된 동안 그 스택은 유지되지만, 호출자에게 반환된 값이 참조 타입이면 호출자가 사용할 때 이미 소멸했을 수 있습니다.

// ❌ 위험
Generator<const std::string&> bad() {
    std::string local = "hello";
    co_yield local;  // local의 참조 반환
}  // 코루틴 일시 정지 — local은 프레임에 있음
// 하지만 호출자가 *it를 쓰는 시점에 "const string&"이
// 프레임 내 local을 가리키므로, 이 경우는 유효할 수 있음.
// 문제: Generator가 이동되거나 소멸되면 dangling
// ✅ 안전: 값으로 반환
Generator<std::string> good() {
    std::string local = "hello";
    co_yield local;  // 복사본 반환
}

6. 반복 begin() 호출

원인: begin()을 여러 번 호출하면 코루틴이 여러 번 resume되어 순회가 꼬입니다. 범위 기반 for는 begin()을 한 번만 호출하므로 안전합니다.

// ❌ 위험: begin() 두 번 호출
auto gen = count(5);
auto it1 = gen.begin();
auto it2 = gen.begin();  // it1과 handle 공유 → 순회 꼬임
// ✅ 안전
for (int x : count(5)) { /* ....*/ }

7. 재귀 Generator와 스택 오버플로우

원인: dfs(child)처럼 재귀 호출 시 깊은 트리에서 스택 오버플로우. 코루틴 프레임은 힙에 있지만 for 루프는 호출 스택을 사용합니다. 깊이 제한을 두거나 반복+스택으로 전환하세요.

8. take() off-by-one 오류

원인: count > ncount >= n 혼동 시 yield 개수 오류.

// ❌ count > n → n=5일 때 6개 yield
if (count > n) break;
// ✅ count >= n → 정확히 n개
if (count >= n) break;

7. 모범 사례 (Best Practices)

항목권장비권장
타입값 타입 Generator<std::string>참조 Generator<std::string_view> (lifetime 위험)
무한 시퀀스take() 또는 루프 내 break상한 없이 순회
이동filter(std::move(nums), pred) 후 nums 미사용이동 후 원본 재사용
예외호출 측 try/catch예외 무시
성능파이프라인 단계 최소화, co_yield std::move(x)filter→map→take 과다 연결

8. 성능 비교: generator vs vector

시나리오: 0 ~ 1,000,000 정수 시퀀스

방식메모리 (대략)초기 지연10개만 사용 시
vector<int>~4MB전체 할당 완료까지4MB 유지, 99만 9,990개 낭비
Generator<int>~수백 바이트 (프레임)거의 없음10개만 계산

벤치마크 예시

#include <chrono>
#include <numeric>
#include <vector>
// 벡터: 100만 개 전부 할당
void bench_vector() {
    auto start = std::chrono::high_resolution_clock::now();
    std::vector<int> vec(1'000'000);
    std::iota(vec.begin(), vec.end(), 0);
    int sum = 0;
    for (int i = 0; i < 10; ++i) sum += vec[i];
    auto end = std::chrono::high_resolution_clock::now();
    // 할당 + 10개 접근
}
// Generator: 10개만 계산
void bench_generator() {
    auto start = std::chrono::high_resolution_clock::now();
    auto gen = count(1'000'000);
    int sum = 0;
    int n = 0;
    for (int x : gen) {
        sum += x;
        if (++n >= 10) break;
    }
    auto end = std::chrono::high_resolution_clock::now();
    // 10번 co_yield만
}

예상 결과 (환경에 따라 다름):

  • vector: 초기 할당 수 ms ~ 수십 ms
  • Generator: 수 μs ~ 수십 μs (10개만 계산)

언제 어떤 방식을 쓸까?

상황권장
전체 순회가 확정vector (캐시 친화적, 단순)
앞 N개만 필요Generator
무한/매우 큰 시퀀스Generator
요소 타입이 무거움Generator (복사 최소화)
랜덤 접근 필요vector

메모리 사용량 비교 다이어그램

flowchart LR
    subgraph vec["vector (100만 int)"]
        V1[4MB 힙]
    end
    subgraph gen["Generator (100만)"]
        G1[코루틴 프레임 ~1KB]
    end

성능 최적화 팁

1. 작은 타입 사용

co_yield할 때마다 값이 promise의 current_value에 복사됩니다. int, double 같은 작은 타입은 복사 비용이 거의 없습니다. std::string은 이동 의미론을 활용해 co_yield std::move(str)로 넘기면 복사 대신 이동이 일어납니다.

// ✅ 이동으로 복사 비용 절감
Generator<std::string> lines() {
    std::string line;
    while (std::getline(file, line)) {
        co_yield std::move(line);  // line은 다음 반복에서 새로 채워짐
    }
}

2. 파이프라인 단계 최소화

filter → map → take처럼 여러 Generator를 연결하면, 각 값마다 여러 코루틴이 resume/suspend를 반복합니다. 단계가 많을수록 오버헤드가 커집니다. 가능하면 한 Generator에서 여러 조건을 처리하는 편이 빠릅니다.

// ❌ 3단계: filter → map → take (각 값마다 3번 suspend/resume)
auto r = take(map(filter(count(100), pred), f), 10);
// ✅ 1단계: 한 Generator에서 처리
Generator<int> combined() {
    int n = 0;
    for (int i = 0; i < 100 && n < 10; ++i) {
        if (pred(i)) { co_yield f(i); ++n; }
    }
}

3. 전체 순회가 확정이면 vector

앞 N개만 쓰는 게 아니라 전체를 여러 번 순회하거나, 랜덤 접근이 필요하면 vector가 캐시 친화적이고 단순합니다. Generator는 “필요한 만큼만”이 명확할 때 선택하세요.

4. 인라인과 LTO

Generator의 resume()/yield_value() 호출이 인라인되면 분기 비용이 줄어듭니다. -O2 이상과 LTO(Link Time Optimization)를 사용하세요.

g++ -std=c++20 -O3 -flto -o app main.cpp

5. 벤치마크 작성 시 주의

Generator는 “첫 값까지의 지연”과 “값 하나당 비용”을 따로 측정하는 것이 좋습니다. 전체 순회 시간만 보면 컴파일러 최적화에 따라 결과가 달라질 수 있습니다.

// 의미 있는 벤치마크: 10개만 꺼내는 시간
auto start = std::chrono::high_resolution_clock::now();
int sum = 0, n = 0;
for (int x : count(1'000'000)) {
    sum += x;
    if (++n >= 10) break;
}
auto end = std::chrono::high_resolution_clock::now();

9. 프로덕션 패턴: 파일 읽기, 데이터 스트리밍

파일 라인별 읽기

대용량 파일을 한 줄씩 스트리밍으로 읽을 때 Generator를 쓰면 메모리를 절약할 수 있습니다.

#include <fstream>
#include <string>
Generator<std::string> readLines(const std::string& path) {
    std::ifstream f(path);
    if (!f) {
        throw std::runtime_error("Cannot open: " + path);
    }
    std::string line;
    while (std::getline(f, line)) {
        co_yield line;
    }
}
// 사용: 10GB 로그 파일도 한 줄씩만 메모리에
for (const auto& line : readLines("huge.log")) {
    if (line.find("ERROR") != std::string::npos) {
        processError(line);
    }
}

CSV 파싱 스트리밍

Generator<std::vector<std::string>> parseCsv(const std::string& path) {
    std::ifstream f(path);
    std::string line;
    while (std::getline(f, line)) {
        std::vector<std::string> row;
        // 간단한 CSV 파싱 (쉼표 구분)
        size_t pos = 0;
        while (pos < line.size()) {
            auto end = line.find(',', pos);
            if (end == std::string::npos) end = line.size();
            row.push_back(line.substr(pos, end - pos));
            pos = end == line.size() ? end : end + 1;
        }
        co_yield std::move(row);
    }
}

네트워크 청크 스트리밍

// 개념적 예: 청크 단위로 데이터 수신
Generator<std::vector<std::byte>> receiveChunks(Socket& sock, size_t chunk_size) {
    std::vector<std::byte> buffer(chunk_size);
    while (true) {
        size_t n = sock.read(buffer.data(), chunk_size);
        if (n == 0) break;
        buffer.resize(n);
        co_yield std::move(buffer);
        buffer.resize(chunk_size);
    }
}

배치 처리 파이프라인

// 로그 파일 → 필터(ERROR만) → 파싱 → 상위 100개
auto lines = readLines("app.log");
auto errors = filter(std::move(lines),
     {
        return s.find("ERROR") != std::string::npos;
    });
auto parsed = map(std::move(errors), parseLogLine);
int count = 0;
for (auto& entry : parsed) {
    handleError(entry);
    if (++count >= 100) break;
}

에러 처리·재시도·메트릭

  • 예외: readLines에서 throw → 호출 측 try/catch로 처리
  • 재시도: max_retries만큼 파일 열기 재시도 후 sleep_for 대기
  • 백프레셔: Generator는 pull 기반 — next() 호출 시에만 생산되므로 자연스럽게 적용
  • 메트릭: withMetrics(gen, count)count.fetch_add(1) 래핑
  • 배치: sliding_window(lines, 100)으로 100개씩 묶어 처리
  • 에러 복구: tryParse 실패 시 logWarning 후 건너뛰기

체크리스트

  • 파일 열기 실패 시 예외 또는 std::optional 처리
  • Generator가 파일/스트림을 소유할 때 수명 관리 확인
  • 대용량 시퀀스는 take로 상한 두기
  • string_view 대신 string 등 값 타입 사용 (lifetime 안전)
  • 불안정한 I/O는 재시도 로직 추가
  • 프로덕션에서는 처리량 메트릭 수집 고려
  • 파싱 실패 시 건너뛰기 vs 종료 정책 결정

10. 정리

항목내용
문제벡터는 전체 할당 → 메모리·계산 낭비
Generatorco_yield로 값 하나씩 lazy 생성
구현promise_type (yield_value, initial/final_suspend) + iterator
무한break/개수 제한으로 조절
조합filter, map, take, enumerate, sliding_window, flatten, distinct
lifetime참조 대신 값, 이동 후 원본 사용 금지
오류dangling 참조, 재귀 스택 오버플로우, 반복 begin() 금지
성능작은 타입·이동 활용, 파이프라인 단계 최소화, LTO
실전파일 라인별 읽기, CSV, 재시도, 메트릭, 배치 처리

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

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

  • C++20 Coroutine | co_await·co_yield로 “콜백 지옥” 탈출하기
  • C++ 비동기 작업과 Coroutine | co_await로 콜백 지옥 탈출하기 [#23-3]
  • C++ 캐시 최적화 | 메모리 접근 패턴 바꿔서 성능 10배 향상시키기

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

C++ Generator, co_yield, lazy 시퀀스, 제너레이터, promise_type, 코루틴 제너레이터, 메모리 최적화 등으로 검색하시면 이 글이 도움이 됩니다.

자주 묻는 질문 (FAQ)

Q. C++23 std::generator와 차이는?

A. C++23 std::generator는 표준 라이브러리에 포함된 제너레이터 타입입니다. yield_value로 참조를 반환할 수 있어 복사 비용을 줄일 수 있습니다. C++20 환경에서는 이 글의 구현을 사용하거나 cppcoro를 활용할 수 있습니다.

Q. Generator를 스레드에서 써도 되나요?

A. Generator 객체 자체는 스레드 안전하지 않습니다. 한 스레드에서 begin()/end()로 순회하는 동안 다른 스레드에서 같은 Generator를 사용하면 데이터 레이스가 발생합니다. 스레드마다 별도 Generator를 만들거나, 동기화를 추가해야 합니다.

Q. 예외가 발생하면 어떻게 되나요?

A. unhandled_exception()에서 std::current_exception()을 저장하고, operator++()begin()에서 std::rethrow_exception으로 호출자에게 전파합니다. 호출하는 쪽에서 try/catch로 처리할 수 있습니다. 한 줄 요약: co_yield로 lazy 시퀀스를 만들면 메모리와 계산을 절약할 수 있습니다. Generator 구현, 무한 시퀀스, 조합, lifetime 주의사항, 성능 비교, 파일·스트리밍 패턴까지 실전에 바로 쓸 수 있습니다. 이전 글: C++ 코루틴 기초 (#23-1) 다음 글: C++ 비동기 코루틴 (#23-3)

관련 글

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

이 부록은 앞선 본문에서 다룬 주제(「C++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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++ Generator 완벽 가이드 | co_yield로 lazy 시퀀스·무한 수열·파이프라인 만들기」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  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 순서를 권장합니다.