[개발 깨알 상식_Tips] / JSONL 빈 줄과 BOM이 만든 배치 파싱 오류.md

JSONL 빈 줄과 BOM이 만든 배치 파싱 오류

조회

<!doctype html>

2026년 6월 18일 | 개발 깨알 상식_Tips


JSONL 배치 입력에서 BOM, 빈 줄, 레코드 경계, 최소 스키마를 순서대로 확인하는 도식
파싱 실패를 줄 번호 하나로 보지 않고, 입력 파일의 네 가지 경계로 나눠 확인한다.

JSONL 파일은 한 줄이 레코드라는 약속 덕분에 배치 로그를 다루기 편하다. 그런데 그 약속이 아주 작게 깨질 때가 있다. 중간에 빈 줄 하나가 끼거나, 파일 첫머리에 UTF-8 BOM이 남거나, 마지막 레코드가 쓰이다가 끊기는 경우다. 로그에는 대개 몇 번째 줄 파싱 실패처럼만 찍히는데, 실제 원인은 줄 내용보다 파일 경계 쪽에 붙어 있을 때가 많았다.

나는 예전에는 이런 오류를 보면 실패한 줄만 복사해서 JSON formatter에 넣었다. 그 줄이 멀쩡하면 “그럼 어디가 문제지?” 하고 전체 파일을 다시 만들었다. 몇 번 반복하고 나니, JSONL은 본문 값보다 레코드 경계와 파일 앞뒤의 쓰레기 문자를 먼저 보는 편이 훨씬 빠르다는 쪽으로 습관이 바뀌었다.

특히 평가 로그처럼 수만 줄이 쌓이는 파일에서는 한 번 잘못 읽기 시작하면 뒤쪽 오류가 전부 연쇄로 보인다. 실제로는 첫 줄 앞의 한 글자, 중간 blank line 하나, 마지막 flush 실패 하나가 전부였는데, 나는 모델 출력이 깨진 줄 알고 한참을 다른 곳에서 헤맨 적이 있다.

실패 줄만 보면 놓치는 세 가지

첫 번째는 BOM이다. Windows 쪽 도구나 스프레드시트 변환기를 거친 파일에서 가끔 첫 줄 맨 앞에 눈에 안 보이는 문자가 남는다. 파서는 이것을 JSON 객체의 첫 글자처럼 읽고, 사람은 화면에서 거의 못 본다. 첫 줄을 아무리 다시 봐도 중괄호가 정상으로 보이니 허무한 쪽이다.

두 번째는 빈 줄이다. 어떤 파서는 빈 줄을 조용히 건너뛰고, 어떤 파서는 그것도 하나의 레코드로 보고 바로 실패한다. 개발 환경에서는 넘어가던 파일이 운영 배치에서는 깨지는 이유가 여기서 생긴다. 그래서 JSONL을 받을 때는 “빈 줄을 허용하는가”가 아니라 우리 파이프라인은 빈 줄을 어떤 신호로 정의하는가를 먼저 정해야 한다.

내 기준은 단순하다. 사람이 편집해서 들어온 파일이면 빈 줄을 정리 단계에서 제거하되, 시스템이 만든 로그라면 빈 줄 자체를 쓰기 실패나 변환 실패의 신호로 본다. 조용히 건너뛰면 당장은 편하지만, 나중에는 누락된 레코드 수를 맞추는 데 더 오래 걸린다.

세 번째는 잘린 마지막 줄이다. 로그 수집기나 중간 저장 단계가 끊기면 마지막 줄에 닫는 따옴표나 중괄호가 없는 상태로 남는다. 이때 실패 위치는 마지막 줄로 나오지만, 원인은 해당 레코드의 값이 아니라 파일을 쓰던 단계의 중단이다.

처음부터 전체 파일을 돌리지 않는다

내가 먼저 보는 것은 전체 row count가 아니다. 작은 앞부분과 끝부분 샘플이다. 첫 몇 바이트에 BOM이 있는지, 처음 10줄 안에 빈 줄이 있는지, 마지막 줄이 닫혀 있는지를 분리해서 본다. 이 단계에서 걸리면 배치 전체를 다시 돌릴 필요가 없다. 입력 파일을 만든 쪽과 읽는 쪽의 계약만 다시 맞추면 된다.

check 1: 첫 줄 앞의 보이지 않는 문자
check 2: 중간 blank line 존재 여부
check 3: 마지막 레코드가 완전한 JSON인지
check 4: 각 레코드에 최소 key가 있는지

이 네 가지를 한 번에 “JSONL 검증”이라고 뭉개면 디버깅이 느려진다. 나는 BOM과 빈 줄은 파일 위생 문제, 마지막 줄 손상은 쓰기 중단 문제, 필수 key 누락은 스키마 문제로 따로 적는다. 같은 파싱 오류여도 고칠 사람이 다르기 때문이다.

예를 들어 데이터 제공자가 만든 export라면 BOM과 빈 줄을 계약서에 넣어야 하고, 내가 만든 collector라면 마지막 줄 flush와 임시 파일 rename 순서를 봐야 한다. 같은 “invalid JSON” 문구라도 수정 위치가 완전히 다르다.

파서의 관대함을 기준으로 삼지 않는다

조금 위험한 착각은 “내 노트북에서는 읽혔다”는 말이다. Python으로 직접 읽을 때 빈 줄을 건너뛰는 코드를 썼다면, 그건 파일이 정상이라는 뜻이 아니다. 내가 임시로 관대하게 읽은 것이다. 반대로 배치 프레임워크가 엄격하게 실패한다고 해서 그쪽이 이상한 것도 아니다. JSONL처럼 단순한 포맷일수록 허용 범위를 느슨하게 두면 뒤 단계에서 더 이상한 오류가 난다.

그래서 저장소에는 가능하면 작은 샘플 파일을 하나 둔다. 정상 2줄, 빈 줄 1개, BOM이 붙은 첫 줄, 잘린 마지막 줄을 따로 만들어 두면 새 파서를 붙일 때 반응을 빠르게 볼 수 있다. 특히 외부 데이터 공급자가 바뀌거나 로그 exporter를 바꾸는 날에는 이 샘플이 생각보다 시간을 많이 아껴 준다.

스키마 검증은 파싱 다음이다

파싱이 통과했다고 바로 안전한 입력은 아니다. 각 줄이 JSON으로 읽힌 뒤에는 최소 key와 타입을 본다. 예를 들어 id는 문자열이어야 하는지, text는 빈 문자열을 허용하는지, labels는 없을 수 있는지 같은 기준이다. 여기까지 와야 비로소 모델 평가나 검색 인덱싱 쪽으로 넘길 수 있다.

중요한 건 순서다. BOM과 빈 줄을 잡기 전에 스키마 오류를 많이 출력하면, 에러 메시지가 섞인다. 파서가 실제로 읽은 객체가 무엇인지 확정하지 못한 상태에서 key 누락을 논하면 헷갈린다. 파일 경계, 레코드 경계, 스키마 경계를 차례대로 좁히는 편이 더 덜 흔들린다.

이 순서를 지키면 실패 보고도 짧아진다. “17만 번째 줄에서 깨짐”이 아니라 “첫 줄 BOM 제거 필요”, “중간 blank line 3개”, “마지막 레코드 incomplete”처럼 말할 수 있다. 그러면 다시 돌릴 작업도 전체 재생성이 아니라 특정 단계의 재export나 cleanup으로 줄어든다.

내가 남기는 기준

JSONL을 주고받는 작업에서는 “한 줄씩 JSON이다”라는 설명만 남기지 않는다. 빈 줄은 실패로 볼지, BOM은 제거할지, 마지막 newline은 필수인지, 잘린 마지막 레코드는 버릴지 중단할지까지 적어 둔다. 이 기준이 없으면 같은 파일을 놓고 누군가는 깨끗하다고 하고, 누군가는 손상됐다고 말한다.

작은 포맷일수록 합의가 작게 빠진다. JSONL 파싱 오류를 몇 번 겪고 나서 내가 얻은 결론은 단순하다. 값이 이상한지 보기 전에, 줄이 정말 레코드인지부터 확인해야 한다. 그 한 단계가 있으면 배치 전체를 다시 돌리는 횟수가 꽤 줄어든다.

검증 스크립트도 거창할 필요는 없다. 처음에는 실패를 세 종류로 나눠 출력하는 정도면 충분하다. 파일 앞부분 문제, 레코드 경계 문제, 스키마 문제를 분리해서 남기면 다음 사람이 같은 파일을 열 때 출발점이 훨씬 또렷해진다. 나는 이 메시지 한 줄 때문에 야간 배치 실패를 데이터 오류가 아니라 입력 계약 문제로 바로 돌려본 적이 여러 번 있다. 특히 재시도 전에 원인 범주가 보이면 불필요한 전체 재처리를 줄일 수 있다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.