Go에서 JSONL: bufio, json.Decoder & 동시성
Go에서 JSONL(JSON Lines) 파일 작업을 위한 완전 가이드입니다. 표준 라이브러리의 bufio, encoding/json, goroutine을 사용하여 JSONL 데이터를 읽고, 쓰고, 스트리밍하고, 동시에 처리하는 방법을 배우세요.
최종 업데이트: 2026년 2월
왜 Go로 JSONL을?
Go는 성능과 동시성이 중요한 JSONL 파일 처리에 탁월한 선택입니다. 표준 라이브러리가 필요한 모든 것을 제공합니다: 효율적인 줄 단위 I/O를 위한 bufio, 파싱 및 직렬화를 위한 encoding/json, 병렬 처리를 위한 goroutine. 서드파티 의존성이 필요 없습니다. Go의 컴파일 특성은 JSONL 파이프라인이 동등한 Python이나 Node.js 스크립트보다 훨씬 빠르게 실행되며, 보통의 하드웨어에서도 초당 수백만 줄을 처리할 수 있음을 의미합니다.
JSONL(JSON Lines)은 줄당 하나의 JSON 객체를 저장하므로 Go의 스트리밍 I/O 모델에 자연스럽게 적합합니다. 전체 파일을 메모리에 로드하는 대신 스캐너나 디코더를 사용하여 레코드를 하나씩 읽고 처리할 수 있습니다. Go의 경량 goroutine과 채널을 결합하면 사용 가능한 모든 CPU 코어를 활용하는 동시 JSONL 파이프라인을 구축할 수 있습니다. 이 가이드에서는 bufio.Scanner로 JSONL 읽기, json.Decoder로 스트리밍, 효율적인 JSONL 쓰기, 레코드 동시 처리, 강력한 오류 처리 방법을 배우게 됩니다.
bufio.Scanner로 JSONL 파일 읽기
Go에서 JSONL을 읽는 가장 간단한 방법은 bufio.Scanner를 사용하는 것입니다. 파일을 줄 단위로 읽고, 각 줄을 json.Unmarshal로 파싱합니다. 이 접근 방식은 버퍼 크기와 오류 처리에 대한 완전한 제어를 제공합니다.
파일을 열고, 스캐너를 생성하고, 줄 단위로 순회합니다. 각 줄은 json.Unmarshal을 사용하여 map 또는 struct로 파싱됩니다. 이 방식은 메모리 사용량을 전체 파일이 아닌 단일 레코드에 비례하게 유지합니다.
package mainimport ("bufio""encoding/json""fmt""log""os")func main() {file, err := os.Open("data.jsonl")if err != nil {log.Fatal(err)}defer file.Close()var records []map[string]anyscanner := bufio.NewScanner(file)// Increase buffer for lines longer than 64KBscanner.Buffer(make([]byte, 0, 1024*1024), 1024*1024)for scanner.Scan() {var record map[string]anyif err := json.Unmarshal(scanner.Bytes(), &record); err != nil {log.Printf("skipping invalid JSON: %v", err)continue}records = append(records, record)}if err := scanner.Err(); err != nil {log.Fatal(err)}fmt.Printf("Loaded %d records\n", len(records))}
프로덕션 코드에서는 제네릭 map 대신 타입이 지정된 Go 구조체로 JSONL 레코드를 파싱하세요. 이를 통해 컴파일 타임 타입 안전성, 더 나은 성능, 더 명확한 코드를 얻을 수 있습니다. 필드 매핑을 제어하기 위해 json 태그가 있는 구조체를 정의하세요.
package mainimport ("bufio""encoding/json""fmt""log""os")type User struct {ID int `json:"id"`Name string `json:"name"`Email string `json:"email"`Age int `json:"age,omitempty"`}func readUsers(path string) ([]User, error) {file, err := os.Open(path)if err != nil {return nil, fmt.Errorf("open file: %w", err)}defer file.Close()var users []Userscanner := bufio.NewScanner(file)lineNum := 0for scanner.Scan() {lineNum++var u Userif err := json.Unmarshal(scanner.Bytes(), &u); err != nil {return nil, fmt.Errorf("line %d: %w", lineNum, err)}users = append(users, u)}if err := scanner.Err(); err != nil {return nil, fmt.Errorf("scanner error: %w", err)}return users, nil}func main() {users, err := readUsers("users.jsonl")if err != nil {log.Fatal(err)}for _, u := range users {fmt.Printf("%s (%s)\n", u.Name, u.Email)}}
json.Decoder로 스트리밍
고성능 스트리밍을 위해 json.Decoder는 중간 줄 분할 없이 io.Reader에서 직접 읽습니다. 내부적으로 버퍼링을 처리하며, 최소한의 할당으로 대용량 JSONL 파일이나 네트워크 스트림에 권장되는 접근 방식입니다.
json.NewDecoder는 모든 io.Reader를 래핑하고 JSON 값을 순차적으로 디코딩합니다. Decode를 호출할 때마다 정확히 하나의 JSON 객체를 읽습니다. 스트림이 끝나면 io.EOF를 반환합니다. bufio.Scanner + json.Unmarshal보다 효율적인데, 각 줄을 별도의 버퍼에 복사하는 과정을 생략하기 때문입니다.
package mainimport ("encoding/json""errors""fmt""io""log""os")type LogEntry struct {Timestamp string `json:"timestamp"`Level string `json:"level"`Message string `json:"message"`Service string `json:"service"`}func processJSONLStream(path string) error {file, err := os.Open(path)if err != nil {return fmt.Errorf("open: %w", err)}defer file.Close()decoder := json.NewDecoder(file)var count intfor {var entry LogEntryerr := decoder.Decode(&entry)if errors.Is(err, io.EOF) {break}if err != nil {return fmt.Errorf("decode record %d: %w", count+1, err)}count++// Process each record as it's decodedif entry.Level == "ERROR" {fmt.Printf("[%s] %s: %s\n",entry.Timestamp, entry.Service, entry.Message)}}fmt.Printf("Processed %d log entries\n", count)return nil}func main() {if err := processJSONLStream("logs.jsonl"); err != nil {log.Fatal(err)}}
Go에서 JSONL 파일 쓰기
Go에서 JSONL 쓰기는 간단합니다: 각 레코드를 json.Marshal로 직렬화하고, 개행 바이트를 추가하고, 파일에 씁니다. 최고의 성능을 위해 파일 writer를 bufio.Writer로 래핑하여 시스템 호출을 줄이세요.
json.Marshal을 사용하여 각 레코드를 JSON 바이트로 직렬화한 다음 개행 문자와 함께 씁니다. 이 접근 방식은 간단하며 소규모에서 중간 크기의 파일에 잘 작동합니다.
package mainimport ("encoding/json""fmt""log""os")type Product struct {ID int `json:"id"`Name string `json:"name"`Price float64 `json:"price"`}func main() {products := []Product{{ID: 1, Name: "Laptop", Price: 999.99},{ID: 2, Name: "Mouse", Price: 29.99},{ID: 3, Name: "Keyboard", Price: 79.99},}file, err := os.Create("products.jsonl")if err != nil {log.Fatal(err)}defer file.Close()for _, p := range products {data, err := json.Marshal(p)if err != nil {log.Printf("skip marshal error: %v", err)continue}data = append(data, '\n')if _, err := file.Write(data); err != nil {log.Fatal(err)}}fmt.Printf("Wrote %d records to products.jsonl\n", len(products))}
고처리량 쓰기를 위해 json.NewEncoder와 bufio.Writer를 함께 사용하세요. 인코더는 각 Encode 호출 후 자동으로 개행을 추가하고, 버퍼링된 writer는 작은 쓰기를 큰 시스템 호출로 일괄 처리합니다. 파일을 닫기 전에 항상 Flush를 호출하세요.
package mainimport ("bufio""encoding/json""fmt""log""os")type Event struct {Type string `json:"type"`Timestamp int64 `json:"timestamp"`Payload map[string]any `json:"payload"`}func writeEvents(path string, events []Event) error {file, err := os.Create(path)if err != nil {return fmt.Errorf("create file: %w", err)}defer file.Close()writer := bufio.NewWriter(file)encoder := json.NewEncoder(writer)// Disable HTML escaping for cleaner outputencoder.SetEscapeHTML(false)for i, event := range events {if err := encoder.Encode(event); err != nil {return fmt.Errorf("encode event %d: %w", i, err)}}// Flush buffered data to fileif err := writer.Flush(); err != nil {return fmt.Errorf("flush: %w", err)}fmt.Printf("Wrote %d events to %s\n", len(events), path)return nil}func main() {events := []Event{{Type: "click", Timestamp: 1700000001, Payload: map[string]any{"x": 120, "y": 450}},{Type: "view", Timestamp: 1700000002, Payload: map[string]any{"page": "/home"}},}if err := writeEvents("events.jsonl", events); err != nil {log.Fatal(err)}}
Goroutine을 이용한 동시 JSONL 처리
Go의 goroutine과 채널을 사용하면 동시 JSONL 파이프라인을 쉽게 구축할 수 있습니다. fan-out 패턴은 파일에서 레코드를 읽고, 여러 워커 goroutine에 분배하고, 채널을 통해 결과를 수집합니다. 이 방식은 대용량 JSONL 파일에 대한 CPU 집약적 변환에 이상적입니다.
package mainimport ("bufio""encoding/json""fmt""log""os""runtime""sync")type Record struct {ID int `json:"id"`Data map[string]any `json:"data"`}type Result struct {ID int `json:"id"`Processed bool `json:"processed"`Score int `json:"score"`}func process(r Record) Result {// Simulate CPU-bound workscore := len(r.Data) * r.IDreturn Result{ID: r.ID, Processed: true, Score: score}}func main() {file, err := os.Open("records.jsonl")if err != nil {log.Fatal(err)}defer file.Close()numWorkers := runtime.NumCPU()jobs := make(chan Record, numWorkers*2)results := make(chan Result, numWorkers*2)// Start workersvar wg sync.WaitGroupfor i := 0; i < numWorkers; i++ {wg.Add(1)go func() {defer wg.Done()for record := range jobs {results <- process(record)}}()}// Close results channel when all workers finishgo func() {wg.Wait()close(results)}()// Read file and send records to workersgo func() {scanner := bufio.NewScanner(file)for scanner.Scan() {var r Recordif err := json.Unmarshal(scanner.Bytes(), &r); err != nil {log.Printf("skip invalid line: %v", err)continue}jobs <- r}close(jobs)}()// Collect and write resultsoutFile, err := os.Create("results.jsonl")if err != nil {log.Fatal(err)}defer outFile.Close()writer := bufio.NewWriter(outFile)encoder := json.NewEncoder(writer)var count intfor result := range results {if err := encoder.Encode(result); err != nil {log.Printf("encode error: %v", err)}count++}writer.Flush()fmt.Printf("Processed %d records with %d workers\n", count, numWorkers)}
이 fan-out 패턴은 CPU 코어 수만큼의 goroutine에 JSONL 레코드를 분배합니다. 버퍼링된 채널은 goroutine이 전송이나 수신에서 블로킹되는 것을 방지합니다. sync.WaitGroup은 결과 채널이 닫히기 전에 모든 워커가 완료되도록 보장합니다. API 호출과 같은 I/O 집약적 작업의 경우 numWorkers를 CPU 수보다 높게 설정할 수 있습니다. 동시 처리에서는 출력 순서가 보장되지 않는다는 점에 유의하세요.
오류 처리 모범 사례
Go에서 강력한 JSONL 처리를 위해서는 세심한 오류 처리가 필요합니다. 실제 JSONL 파일에는 잘못된 형식의 줄, 인코딩 문제, 예상치 못하게 큰 레코드가 포함될 수 있습니다. 이 패턴은 줄 수준 오류 추적, 설정 가능한 건너뛰기 동작, 요약 보고를 제공합니다.
package mainimport ("bufio""encoding/json""fmt""log""os")type ParseError struct {Line intErr errorRaw string}func (e *ParseError) Error() string {return fmt.Sprintf("line %d: %v", e.Line, e.Err)}type JSONLReader struct {scanner *bufio.ScannerlineNum intErrors []ParseErrorSkipped intSuccess int}func NewJSONLReader(file *os.File) *JSONLReader {scanner := bufio.NewScanner(file)scanner.Buffer(make([]byte, 0, 1024*1024), 10*1024*1024)return &JSONLReader{scanner: scanner}}func (r *JSONLReader) ReadAll(target any) error {slice, ok := target.(*[]map[string]any)if !ok {return fmt.Errorf("target must be *[]map[string]any")}for r.scanner.Scan() {r.lineNum++line := r.scanner.Bytes()// Skip empty linesif len(line) == 0 {continue}var record map[string]anyif err := json.Unmarshal(line, &record); err != nil {r.Errors = append(r.Errors, ParseError{Line: r.lineNum,Err: err,Raw: string(line),})r.Skipped++continue}*slice = append(*slice, record)r.Success++}return r.scanner.Err()}func (r *JSONLReader) Summary() string {return fmt.Sprintf("Lines: %d | Success: %d | Skipped: %d | Errors: %d",r.lineNum, r.Success, r.Skipped, len(r.Errors),)}func main() {file, err := os.Open("data.jsonl")if err != nil {log.Fatal(err)}defer file.Close()reader := NewJSONLReader(file)var records []map[string]anyif err := reader.ReadAll(&records); err != nil {log.Fatal(err)}fmt.Println(reader.Summary())for _, e := range reader.Errors {log.Printf("Parse error at %s", e.Error())}}
이 JSONLReader 구조체는 줄 번호를 추적하고, 원시 콘텐츠와 함께 파싱 오류를 수집하며, 성공과 실패에 대한 요약을 보고합니다. 10MB 스캐너 버퍼는 비정상적으로 큰 줄도 패닉 없이 처리합니다. 프로덕션에서는 취소를 위한 context.Context 지원, 처리를 중단하는 최대 오류 임계값, 또는 slog을 사용한 구조화된 로깅으로 확장할 수 있습니다.
JSONL 처리를 위한 Go 패키지
Go의 표준 라이브러리는 JSONL 처리에 필요한 모든 구성 요소를 제공합니다. 다음은 세 가지 핵심 접근 방식과 각각을 사용해야 할 때입니다.
bufio.Scanner
유연함표준 줄 단위 리더입니다. 명시적 파싱을 위해 json.Unmarshal과 함께 사용합니다. 버퍼 크기를 제어하거나, 원시 줄을 건너뛰거나 검사하거나, 오류 보고를 위한 줄 번호가 필요할 때 적합합니다. 설정된 버퍼 크기까지의 줄을 처리합니다.
json.Decoder
권장io.Reader에서 직접 읽는 스트리밍 JSON 디코더입니다. 데이터 복사를 생략하므로 Scanner + Unmarshal보다 효율적입니다. 대용량 파일과 네트워크 스트림에 이상적입니다. 명시적인 개행 분할 없이 연속된 JSON 값을 자동으로 처리합니다.
json.Encoder
쓰기io.Writer에 쓰는 스트리밍 JSON 인코더입니다. 각 Encode 호출 후 자동으로 개행을 추가하여 JSONL 출력에 완벽합니다. 고처리량 쓰기를 위해 bufio.Writer와 결합하세요. SetEscapeHTML 및 SetIndent 설정을 지원합니다.
무료 JSONL 도구 사용해 보기
코드를 작성하고 싶지 않으신가요? 무료 온라인 도구로 브라우저에서 바로 JSONL 파일을 보고, 검증하고, 변환하세요.