오류 한 줄을 건너뛰기 전에 원본을 어디에 남길지 정합니다

JSONL 파일을 읽다가 한 줄에서 멈추면, 그 줄만 지우고 다시 실행하고 싶어집니다. 하지만 삭제한 위치와 원문을 남기지 않으면 전체 기록이 몇 개였는지, 어떤 항목을 처리하지 못했는지 설명할 수 없습니다. 반대로 파일 끝까지 예외를 무시하면 정상 기록과 손실된 기록의 경계가 사라집니다. 이번 글의 목표는 모든 줄을 정상처럼 보이게 만드는 것이 아니라, 통과한 기록과 보류한 기록을 분리하고 나중에 다시 확인할 수 있게 하는 것입니다.

JSON Lines 형식은 UTF-8로 한 줄에 하나의 JSON 값을 둡니다. 빈 줄은 값이 아니지만 null은 유효한 JSON 값입니다. 따라서 파싱 성공 여부만으로 업무에 사용할 기록인지 판단할 수는 없습니다. 이 글에서는 이벤트 객체만 받는 별도 계약을 추가했습니다. 문법을 통과한 null도 그 계약에서는 격리됩니다. 형식이 틀린 줄과 업무 스키마가 다른 줄을 같은 이유로 처리하지 않는 것이 핵심입니다. 형식의 규칙과 사이트의 가상 처리 정책은 구분해 적었습니다.

실험은 8행의 작은 가상 바이트열을 대상으로 했습니다. 실제 서비스 로그나 고객 정보를 내려받지 않았고, 외부 API를 호출하지도 않았습니다. 정상 출력과 격리 원장은 새 임시 폴더에 기록한 뒤 정리했습니다. 이 재현은 실패 분류와 원문 보존의 작동을 확인하며, 대용량 스트림의 성능이나 실제 복구 처리량을 측정한 결과는 아닙니다.

서로 다른 실패를 8행에 넣었습니다

정상 객체는 event_id와 정수 value라는 두 필드를 가집니다. 첫 행의 EV-71에는 12, 여섯 번째 행의 EV-74에는 14, 마지막 EV-75에는 15를 넣었습니다. 두 번째 행에는 닫는 중괄호를 빠뜨렸고, 세 번째는 빈 줄로 만들었습니다. 네 번째에는 null, 다섯 번째에는 정수 대신 문자열을 넣었으며, 일곱 번째에는 UTF-8로 읽을 수 없는 FF 바이트를 넣었습니다. 마지막 행 뒤에는 줄바꿈을 붙이지 않아 끝 경계도 따로 확인했습니다.

원본 행주요 입력판정
1EV-71, 정수 12통과
2닫히지 않은 객체JSON 문법 오류
3빈 줄값 없음
4null유효한 JSON, 객체 계약 불일치
5문자열 value자료형 불일치
6EV-74, 정수 14통과
7FF 바이트UTF-8 디코딩 오류
8EV-75, 마지막 줄바꿈 없음통과

이 구성을 고른 이유는 실패 지점을 구별하기 위해서입니다. 문자 해석에 실패한 줄을 JSON 괄호 검사로 고칠 수는 없습니다. 또 문법이 올바른 null을 중괄호가 빠진 줄과 같은 오류라고 설명하면 수신 계약의 문제를 놓칩니다. 결과를 한 가지 실패율로만 압축하기보다 각 행이 어느 단계에서 멈췄는지를 기록해야 다음 조치가 구체적이 됩니다.

UTF-8, JSON 값, 업무 객체라는 세 문을 통과시킵니다

첫 단계는 원래 줄의 바이트를 엄격한 UTF-8로 해석하는 것입니다. 오류가 난 문자를 대체한 뒤 파싱하지 않습니다. 대체 문자가 들어가도 JSON 문법은 통과할 수 있기 때문에 내용이 바뀐 기록이 정상 출력에 섞일 수 있습니다. 시작 BOM도 JSON Lines의 규칙에 따라 허용하지 않는 대상으로 둡니다. 첫 줄이 깨졌을 때 아무 인코딩으로 다시 읽어 통과시키는 방식은 이 실험의 처리 정책에 들어 있지 않습니다.

두 번째 단계에서 JSON 값을 파싱합니다. Python 기본 파서가 일부 비표준 숫자 표현을 받아들일 수 있으므로, 코드에는 parse_constant를 통한 차단 함수를 지정했습니다. 세 번째 단계에서는 값이 객체인지, 필드가 정해진 두 개인지, 식별자가 문자열인지, value가 정수인지 확인합니다. 이 계약은 예제를 위해 정한 것이며 JSONL 형식 자체가 모든 필드의 이름과 자료형을 요구하는 것은 아닙니다. 실제 처리에서는 자신의 스키마와 추가 필드 허용 정책을 명시해야 합니다.

이번 정수 검사는 Python에서 불리언을 정수처럼 취급하는 조건을 피하려고 자료형을 정확히 비교합니다. 값이 참이라는 것과 값이 1이라는 것은 이벤트 수량 계약에서 다를 수 있기 때문입니다. 같은 이유로 숫자처럼 보이는 문자열을 임의로 정수로 바꾸지 않았습니다. 변환이 필요하면 파싱 통과 뒤의 별도 단계로 두고, 원래 값과 변환된 값을 함께 남기는 편이 추적하기 좋습니다.

정상 3행과 보류 5행의 합계가 원본 8행과 맞습니다

실제 실행에서 정상 출력으로 보낸 식별자는 EV-71, EV-74, EV-75였습니다. 격리 원장에는 2·3·4·5·7행이 들어갔습니다. 따라서 정상 3행과 보류 5행을 합치면 입력 8행입니다. 이 합계는 일부 행이 조용히 사라지지 않았는지 보는 기본 검사입니다. 업무에서 같은 이벤트가 두 번 들어왔는지, 이전 배치에서 이미 처리됐는지까지 확인한 결과는 아닙니다. 행의 완전성과 이벤트의 고유성은 별도의 검수 항목입니다.

마지막 줄바꿈이 없는 EV-75도 처리됐습니다. JSON Lines 안내는 마지막 값 뒤 줄바꿈을 권장하지만, 그것이 없다는 이유만으로 값이 무효가 되는 것은 아닙니다. 따라서 파일을 분리할 때 마지막 조각이 줄바꿈으로 끝나지 않았다는 이유만으로 버리지 않아야 합니다. 반대로 입력 끝의 줄바꿈 뒤에 별도 빈 줄이 실제로 들어 있는 경우는 빈 행 처리 정책을 따로 적용해야 합니다. 단순한 줄바꿈 개수와 기록 수가 언제나 같지는 않습니다.

이 실험은 원본 8행의 바이트를 다시 이어 붙였을 때 처음과 동일한지도 확인했습니다. 정상 출력은 JSON을 다시 직렬화하므로 공백과 표현 방식이 원문과 달라질 수 있습니다. 원본 보존 검사와 정상 객체의 의미 보존 검사를 같은 해시 하나로 묶지 않는 이유입니다. 어떤 목적의 비교인지 먼저 정해야 정상적인 재직렬화를 손상으로 오해하지 않습니다.

행 번호만으로 부족해서 바이트 위치와 원문도 남겼습니다

격리 항목에는 원본 행 번호, 바이트 오프셋, 오류 이유, 해당 줄의 SHA-256와 원문 바이트의 Base64 표현을 넣었습니다. UTF-8 자체가 잘못된 줄은 문자열 필드로 그대로 옮기기 어렵기 때문에 바이트를 보존하는 표현이 필요했습니다. Base64는 암호화나 익명화가 아닙니다. 복원 가능한 표현이므로 실제 로그에 개인정보가 있다면 원본과 같은 수준으로 접근을 제한해야 합니다. 이 글에 공개한 것은 새로 만든 가상 자료뿐입니다.

행 번호는 사람이 오류를 찾는 데 편하고, 바이트 위치는 특정 원본에서 어디에 있던 줄인지 추적하는 데 도움이 됩니다. 다만 원본을 줄바꿈 변환이나 인코딩 변경 후 다시 저장하면 바이트 위치가 달라질 수 있습니다. 격리 원장만 받아 다른 버전의 파일에 위치를 적용하면 엉뚱한 행을 가리킬 수 있습니다. 파일 전체의 해시와 격리 항목을 묶어 보관하는 이유입니다. 원장이 어떤 원본 버전에 해당하는지 확인할 수 있어야 합니다.

실제 자료에 적용할 때 오류 메시지에도 민감한 내용이 들어가는지 확인하세요. 일부 파서나 검증 도구는 원문 일부를 예외에 포함할 수 있습니다. 운영 화면에는 행 번호와 오류 분류만 보여 주고, 상세 원문은 제한된 저장소에 남기는 식으로 역할을 나눌 수 있습니다. 보류 기록을 살릴 수 있게 만드는 것과 모든 사람이 원문을 볼 수 있게 만드는 것은 별개의 선택입니다.

격리한 줄은 원장을 지우지 않고 새 결과로 연결합니다

오류 행을 고친 뒤 정상 출력에 다시 넣으려면 어느 격리 항목에서 나온 수정인지 남겨야 합니다. 원문에 직접 덮어써서 처음부터 깨끗했던 파일처럼 만들면 반복 수신 문제를 조사하기 어렵습니다. 원본 해시와 행 번호, 수정 사유, 수정본 식별자, 재검사 판정을 연결하는 편이 좋습니다. 닫는 괄호를 보완하는 작업이라도 누가 어떤 근거로 값을 확정했는지 설명할 수 있어야 합니다. 문자 몇 개를 고쳤다고 원래 사건의 사실성까지 검증되는 것은 아닙니다.

null을 객체로 바꾸거나 문자열 숫자를 정수로 바꾸는 수정은 단순 문법 교정이 아닙니다. 비어 있는 값이 누락을 뜻하는지, 아직 모르는 값을 뜻하는지, 정상적인 상태 표시인지 제공자에게 확인이 필요할 수 있습니다. 이번 코드는 보류한 다섯 줄을 자동으로 정상화하지 않습니다. 실패를 분리하는 검사와 값의 의미를 확정하는 작업을 섞지 않기 위해서입니다. 자동 수정이 가능한 항목과 사람이 확인해야 하는 항목도 업무 계약으로 나누세요.

여러 번 재처리하면 같은 정상 이벤트가 다시 반영될 수 있습니다. 처리 대상의 고유 식별자와 배치 식별자를 구분하고, 이미 적용된 기록인지 확인하는 절차가 필요합니다. 그 판단을 만들 때는 같은 ID와 내용 충돌을 나누는 기준을 참고할 수 있습니다. JSONL을 정상 행으로 나눴다는 사실만으로 재처리가 안전해지는 것은 아닙니다.

현재 가상 계약은 event_id가 문자열인지만 확인하며 빈 문자열까지 금지하는 규칙은 넣지 않았습니다. 실제 업무가 비어 있지 않은 고유 이벤트 ID를 요구한다면 그 조건을 별도로 추가해야 합니다. 두 필드와 정수라는 검사만 통과한 자료를 완전한 이벤트라고 부르지 않는 이유입니다. 결과를 확장할 때는 빈 ID·같은 ID의 재수신 같은 고정 입력도 새로 준비하세요.

대용량 처리에서는 줄 수보다 자원 한계도 먼저 정합니다

이번 자료는 작은 바이트열 전체를 메모리에 둔 실험입니다. 크기가 큰 실제 파일은 줄을 차례로 읽는 구조와 최대 행 길이, 전체 처리량, 격리 저장 공간의 한도가 필요합니다. 행 단위 형식이라도 한 줄이 매우 크거나 중첩이 지나치면 자원을 많이 사용할 수 있습니다. 정상 줄을 빨리 처리하는 것만으로는 운영 준비가 끝나지 않습니다. 격리 원장이 계속 커질 때 멈추거나 별도 저장소로 넘기는 기준도 함께 정해야 합니다.

정상 출력과 격리 원장을 각각 썼다는 사실이 두 파일의 동시 확정을 보장하지는 않습니다. 실제 처리기가 중간에 종료되면 한쪽만 갱신될 수 있으므로, 배치별 임시 출력과 완료 표식, 재시작 위치를 어떻게 관리할지 검토해야 합니다. 이 글의 임시 파일 기록은 작은 성공 경로를 확인한 것으로, 전원 장애나 파일시스템 오류를 주입한 내구성 실험은 아닙니다. 그런 검증이 필요한 업무라면 별도의 장애 시나리오가 있어야 합니다.

검수 결과를 전달할 때는 입력 행 수, 정상 행 수, 격리 행 수와 원본 식별자를 먼저 적으세요. 문법 오류 몇 건, 스키마 오류 몇 건인지 나누면 자료 제공자도 수정할 지점을 찾기 쉽습니다. JSON 문법과 업무 규칙을 따로 검사하는 가이드는 각 단계의 역할을 더 자세히 설명합니다. 실험 코드와 운영 정책을 연결할 때 무엇을 추가해야 하는지도 확인할 수 있습니다.

모르는 줄을 정상으로 바꾸지 않는 것이 검수의 시작입니다

이 결과는 고정한 8행과 정해진 두 필드 계약에서 얻은 것입니다. 모든 JSONL 생산자의 형식, 압축 파일의 스트리밍 처리, 중복 키와 큰 숫자의 정밀도, 실제 이벤트의 진위를 모두 검증하지 않았습니다. 특히 같은 객체 안의 중복 키는 기본 JSON 파서의 별도 동작을 살펴야 하는 문제입니다. 여기서 정상 출력으로 분류한 것은 이 실험의 검사 단계를 통과했다는 뜻이며, 실제 업무 반영을 승인했다는 뜻은 아닙니다.

처음 적용할 때는 정상 줄과 보류 줄의 합계가 원본과 맞는지, 각 보류 사유가 필요한 정보를 담는지, 원문이 그대로 복원 가능한지부터 보세요. 이후에는 재처리와 접근 권한, 저장 공간의 정책을 자신의 업무에 맞게 추가하면 됩니다. 오류 줄을 없애는 대신 그 줄을 설명할 수 있는 작은 원장을 남기는 것이 다음 수정의 출발점입니다.

입력과 결과를 직접 대조하는 재현 자료

실험 실행일은 2026년 10월 15일이며 이 글의 발행 일정과 구분합니다. Python 3 표준 라이브러리로 가상 자료만 처리했습니다. 아래 파일은 기존 사용자 파일을 인자로 받지 않습니다.

python3 json-lines-quarantine-lab-20261015.py

실행 시각과 런타임은 환경에 따라 달라집니다. 판정 근거는 다운로드 결과의 고정 입력과 각 사례의 실제 출력으로 확인하세요.

규칙을 확인한 공식 원문

아래 원문은 2026년 10월 15일 확인했습니다. 형식과 동작의 설명을 확인한 출처이며, 이 글의 가상 실험이나 실제 업무 성과를 대신 증명하지 않습니다.

  • JSON Lines format — UTF-8, BOM 금지, 한 줄마다 유효한 JSON 값, 빈 줄과 마지막 줄바꿈의 규칙을 확인했습니다.
  • RFC 8259: JSON — null 등 JSON 값과 문법·유니코드 요구를 확인했습니다.
  • Python json — JSONDecodeError와 parse_constant를 이용한 비표준 숫자 차단 동작을 확인했습니다.