Go 1.23의 Generic Iterator 패턴 적용기

문제 상황

레거시 API 서버에서 대용량 데이터를 페이지네이션으로 조회하는 코드가 있었다. 기존에는 모든 페이지를 slice에 담아 반환했는데, 데이터가 커지면서 메모리 부담이 심해졌다.

func FetchAllRecords(limit int) ([]Record, error) {
    var records []Record
    offset := 0
    for {
        batch, err := db.Query(limit, offset)
        if err != nil {
            return nil, err
        }
        if len(batch) == 0 {
            break
        }
        records = append(records, batch...)
        offset += limit
    }
    return records, nil
}

Go 1.23 Iterator 적용

Go 1.23부터 range over function이 지원되면서 iterator 패턴을 구현할 수 있게 되었다. iter.Seq[T] 타입을 활용해 lazy evaluation이 가능해졌다.

func IterateRecords(limit int) iter.Seq[Record] {
    return func(yield func(Record) bool) {
        offset := 0
        for {
            batch, err := db.Query(limit, offset)
            if err != nil {
                return
            }
            if len(batch) == 0 {
                return
            }
            for _, record := range batch {
                if !yield(record) {
                    return
                }
            }
            offset += limit
        }
    }
}

// 사용
for record := range IterateRecords(100) {
    if err := process(record); err != nil {
        break
    }
}

결과

  • 메모리 사용량 70% 감소 (10GB → 3GB)
  • 첫 번째 레코드 처리까지 시간 단축
  • 에러 발생 시 즉시 중단 가능

기존에는 channel로 비슷한 패턴을 구현했었는데, iterator가 훨씬 직관적이고 고루틴 관리 부담도 없었다. 다만 아직 에러 핸들링 패턴은 커뮤니티에서 정립 중인 듯하다.