Go 1.23의 iterator 패턴 도입과 실전 적용기

배경

8월에 출시된 Go 1.23의 가장 큰 변화는 iterator 패턴의 공식 지원이다. 기존에는 채널이나 슬라이스로 데이터를 순회했는데, 이제 iter.Seq를 통해 더 유연한 순회가 가능해졌다.

사내 데이터 파이프라인에서 대용량 로그를 청크 단위로 처리하는 부분이 있었는데, 메모리 이슈로 계속 골치를 썩고 있었다. 이참에 iterator 패턴을 적용해봤다.

기존 코드

func ProcessLogs(filepath string) error {
    logs, err := LoadAllLogs(filepath) // 전체 로딩
    if err != nil {
        return err
    }
    
    for _, log := range logs {
        process(log)
    }
    return nil
}

파일이 몇 GB 단위가 되면 메모리에 전부 올리는 게 부담스러웠다.

Iterator 적용

func LogIterator(filepath string) iter.Seq[LogEntry] {
    return func(yield func(LogEntry) bool) {
        file, _ := os.Open(filepath)
        defer file.Close()
        
        scanner := bufio.NewScanner(file)
        for scanner.Scan() {
            log := ParseLog(scanner.Text())
            if !yield(log) {
                return
            }
        }
    }
}

func ProcessLogs(filepath string) error {
    for log := range LogIterator(filepath) {
        process(log)
    }
    return nil
}

효과

  • 메모리 사용량이 약 80% 감소했다
  • 첫 데이터 처리까지 지연시간이 거의 없어졌다
  • 코드가 더 선언적이고 읽기 좋아졌다

주의사항

yield 함수가 false를 반환하면 순회가 중단된다. 이 시점에 리소스 정리가 제대로 되는지 확인해야 한다. defer를 활용하는 게 안전하다.

또한 기존 라이브러리들이 아직 iterator를 지원하지 않는 경우가 많아서, 어댑터 함수를 직접 작성해야 하는 경우도 있었다.

정리

Go 1.23의 iterator는 실용적이다. 특히 스트리밍 처리나 대용량 데이터 다룰 때 유용했다. 다만 팀 전체가 Go 1.23으로 업그레이드할 때까지는 제한적으로만 사용할 예정이다.