| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
- Let's Encrypt
- vim
- jsonl
- curl
- Java
- vscode
- yaml
- .dockerignore
- getting started
- ndjson
- Thinking Mode
- json
- Typescript
- glm-ocr
- Ollama
- RandomAccessFile
- json schema
- PowerShell
- Docker Compose
- docker
- cli
- podman compose
- 이미지
- Vite
- io
- tauri
- Webpack
- k3s
- podman
- qwen3-coder-next
- Today
- Total
워로디스
JSONL, NDJSON 정리 본문
1. JSONL / NDJSON이란
JSONL(JSON Lines)과 NDJSON(Newline Delimited JSON)은 거의 같은 형식이다.
핵심 규칙은 다음과 같다.
한 줄에 하나의 유효한 JSON value
예:
{"id":1,"text":"hello"}
{"id":2,"text":"world"}
{"id":3,"text":"line1\nline2"}
주로 로그, 이벤트 스트림, 대용량 데이터셋, ML 학습 데이터, 배치 처리 등에 사용된다.
2. JSONL과 NDJSON의 차이
| 항목 | JSONL | NDJSON |
|---|---|---|
| 풀네임 | JSON Lines | Newline Delimited JSON |
| 확장자 | .jsonl |
.ndjson |
| 자주 쓰이는 맥락 | 파일, 데이터셋, ML | 스트리밍 API, HTTP 응답 |
| 실질적 포맷 차이 | 거의 없음 | 거의 없음 |
실무적으로는 이렇게 봐도 된다.
JSONL == NDJSON
다만 HTTP 스트리밍 응답에서는 보통 이런 Content-Type을 쓴다.
Content-Type: application/x-ndjson
3. 줄바꿈 문자가 JSON 안에 들어가는 경우
JSONL에서 레코드는 실제 개행 문자로 구분된다.
따라서 JSON 문자열 안의 줄바꿈은 실제 줄바꿈이 아니라 \n으로 escape되어야 한다.
정상:
{"message":"hello\nworld"}
{"message":"next record"}
비정상:
{"message":"hello
world"}
{"message":"next record"}
JSON 문자열 안에는 실제 개행 문자를 직접 넣을 수 없다. 반드시 \n으로 표현해야 한다.
4. 각 라인의 시작 문자는 {, [로만 한정되지 않는다
JSONL의 각 라인은 object일 필요는 없다.
각 라인은 하나의 유효한 JSON value이면 된다.
가능한 시작 문자:
| 시작 문자 | 의미 |
|---|---|
{ |
object |
[ |
array |
" |
string |
-, 0–9 |
number |
t |
true |
f |
false |
n |
null |
예를 들어 아래는 모두 유효한 JSONL이다.
{"id":1}
[1,2,3]
"hello"
123
true
false
null
즉 null도 포맷상으로는 정합하다.
다만 실무에서는 대부분 object per line을 기대한다.
{"id":1,"message":"hello"}
{"id":2,"message":"world"}
object 형태가 필드 확장, 스키마 관리, downstream 처리에 유리하기 때문이다.
5. 문자열은 반드시 큰따옴표가 필요하다
JSON value가 문자열이면 반드시 double quote로 감싸야 한다.
정상:
"hello"
{"message":"hello"}
비정상:
hello
'hello'
hello는 raw text일 뿐 JSON 문자열이 아니다.
JSON 문자열은 반드시 다음 형태여야 한다.
"hello"
6. Spring Boot RestController의 String 반환과 JSON 문자열은 다르다
Spring Boot에서 String을 반환하면 보통 raw text response가 된다.
@GetMapping("/hello")
public String hello() {
return "hello";
}
응답 body는 보통 이렇게 나온다.
hello
하지만 이것은 JSON 문자열이 아니다.
JSON 문자열이라면 body가 이렇게 나와야 한다.
"hello"
즉 다음은 서로 다르다.
HTTP raw text -> hello
JSON string value -> "hello"
JSONL string record -> "hello"
따라서 JSONL/NDJSON으로 문자열 레코드를 보내려면 아래처럼 quote가 있어야 한다.
"hello"
"world"
보통은 object로 감싸는 것이 더 안전하다.
{"message":"hello"}
{"message":"world"}
7. 성능 관점
JSONL에서 각 JSON을 분리하는 작업은 보통 큰 병목이 아니다.
레코드 구분자는 실제 newline byte인 0x0A이다.
{"id":1,"text":"hello\nworld"}\n
{"id":2,"text":"next"}\n
여기서 문자열 내부의 \n은 실제 개행 byte가 아니라 \와 n 두 문자다.
따라서 레코드 경계를 찾을 때는 실제 newline byte만 찾으면 된다.
성능 병목은 보통 다음 쪽에서 먼저 발생한다.
I/O
압축 해제
UTF-8 디코딩
JSON 파싱
객체 생성
후처리 로직
피해야 할 방식:
data = f.read()
lines = data.splitlines()
권장 방식:
with open("data.jsonl", "rb") as f:
for line in f:
obj = json.loads(line)
성능이 더 중요하면 orjson, simdjson 같은 빠른 파서를 고려할 수 있다.
8. 병렬 처리
JSONL은 큰 파일을 byte range로 나누어 병렬 처리하기 좋다.
각 worker가 자기 시작 위치에서 다음 newline까지 이동하면 레코드 경계를 맞출 수 있다.
반대로 일반 JSON 배열은 중간 offset에서 안전하게 시작하기 어렵다.
[
{"id":1},
{"id":2}
]
이 경우 JSON 구조를 이해해야 하기 때문이다.
단, .jsonl.gz는 gzip 특성상 중간 지점부터 독립적으로 풀기 어려워 병렬 처리에 불리할 수 있다.
9. JVM에서의 처리
JVM에서는 보통 JSONL 전용 파서보다는 다음 조합을 쓴다.
라인 단위 입력 + JSON 파서
가장 무난한 기본 선택은 Jackson이다.
ObjectMapper mapper = new ObjectMapper();
ObjectReader reader = mapper.readerFor(Event.class);
try (BufferedReader br = Files.newBufferedReader(
Path.of("events.jsonl"),
StandardCharsets.UTF_8
)) {
String line;
while ((line = br.readLine()) != null) {
if (line.isBlank()) continue;
Event event = reader.readValue(line);
// process event
}
}
이 방식의 장점:
라인 번호 추적 가능
깨진 레코드 skip 가능
ObjectReader 재사용 가능
구현이 단순함
고성능이 더 필요하면 다음 후보를 벤치마크해볼 수 있다.
| 라이브러리 | 특징 |
|---|---|
| Jackson | 안정적인 기본 선택 |
| DSL-JSON | 고성능 JVM JSON databinding |
| jsoniter-java | 빠른 Java JSON parser |
| simdjson-java | SIMD 기반 고성능 파서 계열 |
10. 실무 권장 형태
대부분의 경우 JSONL/NDJSON은 object per line으로 쓰는 것이 가장 안전하다.
{"id":1,"type":"event","payload":{"message":"hello"}}
{"id":2,"type":"event","payload":{"message":"world"}}
권장사항은 다음과 같다.
1. 한 줄에 하나의 JSON value만 둔다.
2. 문자열 내부 줄바꿈은 \n으로 escape한다.
3. 문자열 value는 반드시 "..."로 감싼다.
4. 실무에서는 object per line을 기본으로 한다.
5. 전체 파일 read + split보다 스트리밍 처리를 사용한다.
6. JVM에서는 Jackson + ObjectReader 재사용이 무난하다.
7. 병목은 줄 분리보다 JSON 파싱, 객체 생성, I/O, 압축 해제 쪽일 가능성이 높다.
8. Spring Boot의 raw String 응답은 JSON 문자열과 다르다.
결론
JSONL/NDJSON의 핵심 원칙은 하나다.
각 물리적 라인은 독립적으로 파싱 가능한 하나의 유효한 JSON value여야 한다.
문자열 레코드는
hello
가 아니라
"hello"
여야 한다.
그리고 일반적인 데이터 레코드는 보통 다음처럼 object 형태로 두는 것이 가장 안전하다.
{"message":"hello"}'정리 > 데이터직렬화형식' 카테고리의 다른 글
| JSON Schema 치트시트 (0) | 2026.05.03 |
|---|---|
| JSON Schema 정리 (0) | 2026.05.03 |
