1. Autodetect and gzip JSONL

문서가 침묵하는 조합을 실험으로 가르다가 첫 결론이 뒤집힌 이야기

  • Haram Lee
  • 2026-09-07
  • studies / Topics / Data Loading

문서에 없는 동작은 어떻게 확인할까?

  • 스키마를 지정하면 BigQuery의 autodetect는 정말 무시될까?
  • 로드가 실패했을 때 원인을 어떻게 한 변수로 좁힐까?
  • 첫 실험의 결론은 얼마나 믿어도 될까?

1.1 The Setup

S3에 쌓인 파일을 BigQuery로 옮기는 파이프라인을 다루고 있었다. 구조는 단순하다. 먼저 GCP의 Storage Transfer Service(STS)가 S3 객체를 GCS 스테이징 버킷으로 복사하고, 이어서 BigQuery load job이 스테이징의 파일을 날짜 파티션에 덮어쓴다.

flowchart LR
  s3["S3<br/>원본 파일"] -->|"STS 복사"| gcs["GCS<br/>스테이징"]
  gcs -->|"load job<br/>WRITE_TRUNCATE"| bq["BigQuery<br/>table$YYYYMMDD"]

어느 날 gzip으로 압축한 JSONL 로그를 적재하고 싶다는 요청이 왔다. 로그에는 request.body라는 필드가 있는데, 요청마다 구조가 달랐다. 어떤 행에서는 객체이고, 어떤 행에서는 배열이나 문자열이고, 비어 있기도 했다. 이런 필드는 BigQuery의 JSON 타입으로 고정해야 한다. 그래서 사용자가 스키마를 직접 지정할 수 있도록 파이프라인 설정에 항목을 추가했고, 실제 적재까지 가는 테스트를 돌렸다.

로드가 실패했다.

text
Failed to parse JSON: No active field found.; ParsedNumber returned false; ...
Parser terminated before end of string File: part-0002.jsonl.gz

파이프라인 코드를 보니 load job에 항상 autodetect=True를 보내고 있었다. 주석에는 “스키마가 있으면 무시된다"고 적혀 있었다.

1.2 First Experiment: Splitting with bq load

STS가 옮긴 GCS 객체는 크기가 원본과 같았고 Content-Encoding도 붙어 있지 않았다. 전송은 정상이었다. 그래서 같은 객체를 bq load로 직접 넣으며 조건을 하나씩 바꿨다.

실험조건결과
A스키마만, GCS gzip 객체 하나적재
B스키마만, 로컬 비압축 JSONL적재
C스키마만, 로컬 gzip적재
D스키마와 autodetect, GCS gzip 객체실패
Eautodetect만, GCS gzip 객체실패
F스키마만, 와일드카드로 여러 파일적재

autodetect를 켜면 실패하고 끄면 성공했다. 오류 문구의 “Parser terminated before end of string"은 압축된 바이트를 JSON으로 읽으려 할 때 나올 법한 문구였다. 그래서 첫 결론은 이랬다. “autodetect가 표본을 읽는 경로는 gzip을 풀지 못한다.”

조치도 바로 했다. 스키마가 있으면 autodetect를 끈다. 코드로는 autodetect = schema is None 한 줄이다.

1.3 Reading the Docs Again

그날 저녁 공식 문서를 다시 읽다가 결론이 흔들렸다. schema auto-detection 문서에 이런 문장이 있었다.

In addition to detecting schema details, auto-detection recognizes the following: Compression — BigQuery recognizes gzip-compatible file compression when opening a file.

문서대로라면 autodetect도 gzip을 열 수 있어야 한다. 같은 문서는 autodetect가 무엇을 하는지도 적어 둔다. 무작위 파일 하나에서 앞쪽 최대 500행을 표본으로 읽고, JSON의 중첩 필드와 반복 필드를 추론한다. 값이 배열이면 repeated 컬럼으로 만든다.

더 중요한 것은 문서에 없는 부분이었다. Jobs API 레퍼런스에서 autodetect의 설명은 “Indicates if we should automatically infer the options and schema for CSV and JSON sources.“이고, schema의 설명은 “The schema for the destination table.“이다. 두 설명은 서로를 언급하지 않는다. 스키마 지정 문서도 둘을 대안으로 소개할 뿐, 둘을 같이 줬을 때 어떻게 되는지는 어디에도 없다. “스키마가 있으면 무시된다"는 주석은 근거가 없었던 셈이다.

여기서 문서로 세울 수 있는 것과 없는 것을 나눴다.

  • 문서로 성립하는 것: gzip JSON 로드는 지원된다. autodetect는 로드와 별개로 표본을 읽는다.
  • 문서가 말하지 않는 것: 스키마와 autodetect를 같이 줬을 때의 동작.
  • 문서와 어긋나는 것: “autodetect가 gzip을 못 푼다"는 내 결론.

그리고 다른 설명 하나가 문서와 더 잘 맞았다. 실험 데이터의 request.body는 행마다 모양이 달랐다. autodetect가 표본에서 이 필드를 RECORD로 추론했다면, 표본과 모양이 다른 행에서 “No active field found” 같은 오류가 날 수 있다. 이 설명이 맞다면 압축하지 않은 파일에서도 같은 오류가 나야 한다. 그런데 첫 실험에는 그 대조군이 없었다.

1.4 Second Experiment: Nine Streams with Controls

다음 날에는 bq load가 아니라 실제 파이프라인 경로 전체에서 변수를 나눴다. 설정부터 STS 전송, load job까지 모두 그대로 거친다. 이미 배포한 규칙이 “스키마가 없으면 autodetect를 켠다"였으므로, 스키마 유무가 곧 autodetect 유무다.

데이터는 두 종류를 만들었다. “simple"은 request.body가 항상 같은 모양의 객체이고, “mixed"는 행마다 객체, 배열, 문자열, null로 바뀐다. 확장자의 영향도 보려고 gzip 바이트를 .jsonl 이름으로 올린 파일도 넣었다.

스트림데이터압축스키마autodetect결과
simple_plainsimple없음없음켬적재
simple_gzsimplegzip없음켬적재
mixed_plainmixed없음없음켬실패
mixed_gzmixedgzip없음켬실패
mixed_plain_schemamixed없음있음끔적재
mixed_gz_schemamixedgzip있음끔적재
gzbytes_noextmixedgzip 바이트, .jsonl 이름없음켬실패
gzbytes_noext_schemamixedgzip 바이트, .jsonl 이름있음끔적재
처음 실패한 로그원래 데이터gzip있음끔적재

결론을 뒤집은 것은 대조군 두 개였다.

flowchart TD
  h["가설: autodetect가 gzip을 못 푼다"]
  h --> c1["simple_gz<br/>gzip + autodetect"]
  h --> c2["mixed_plain<br/>비압축 + autodetect"]
  c1 -->|"적재됨"| r1["gzip은 원인이 아니다"]
  c2 -->|"같은 오류로 실패"| r2["원인은 행마다 다른 모양"]
  • gzip은 원인이 아니다. simple_gz는 autodetect를 켠 채로 gzip을 적재했다. 문서의 문장과 일치한다.
  • 원인은 데이터의 모양이다. mixed_plain은 압축 없이도 같은 오류로 실패했다. autodetect가 표본에서 request.body를 RECORD로 추론하고, 모양이 다른 행에서 깨진다.
  • 확장자는 상관없다. gzip 바이트를 .jsonl 이름으로 올려도 스키마가 있으면 적재됐다. BigQuery는 이름이 아니라 내용을 보고 gzip을 알아본다.

1.5 Same Rule, Different Reason

규칙은 그대로 맞았다. 스키마가 있으면 autodetect를 끈다. 바뀐 것은 이유다. 처음에는 “gzip을 피하려는 임시 우회"였는데, 이제는 “지정한 스키마가 있어도 autodetect가 켜져 있으면 추론이 개입하니까"가 됐다. 첫 실험의 D, 즉 스키마와 autodetect를 같이 준 경우가 실패한 것이 그 증거다.

원칙으로 다시 쓰면 이렇다. load job은 스키마를 한 곳에서만 받아야 한다. 지정 스키마, autodetect, 기존 테이블의 스키마 중 하나다. 둘을 같이 주면 문서가 정의하지 않은 동작에 기대게 된다.

사용자 안내도 바뀌었다. “gzip JSONL은 스키마가 필수"가 아니라 “행마다 모양이 다른 필드는 JSON 타입으로 스키마를 지정하라"다. 코드 주석에서는 동작 원리를 추측한 부분을 빼고 관찰한 사실만 남겼다. 주석에 적은 가설은 실험이 뒤집을 수 있기 때문이다.

1.6 What Is Still Open

스키마 없이 매일 증분 로드를 하는 경우가 남는다. 문서에는 “When all rows in a column are empty, auto-detection will default to STRING data type for the column.“이라는 문장이 있다. 어느 날 한 컬럼의 표본이 전부 null이면 그날 autodetect는 그 컬럼을 STRING으로 추론하고, 기존 테이블의 타입과 어긋나 로드가 실패할 수 있다.

생각해 둔 방향은 autodetect를 테이블을 처음 만드는 로드에서만 켜는 것이다. bq CLI 레퍼런스에 “If no schema is specified, and –autodetect is false, and the destination table exists, then the schema of the destination table is used.“라는 문장이 있으니, 이후 로드는 스키마도 autodetect도 없이 보내 기존 테이블 스키마를 쓰게 할 수 있다. 다만 이것은 아직 돌려 보지 않았다. 문서가 말하는 것과 실제 동작이 다를 수 있다는 것을 이번에 배웠으니, 이것도 대조군과 함께 확인한 뒤에 규칙으로 삼으려 한다.

Discussion