[개발 깨알 상식_Tips] / 큰 JSON을 통째로 열지 않고 훑어보기.md

큰 JSON을 통째로 열지 않고 훑어보기

조회

2026년 5월 20일 | 개발 깨알 상식_Tips


큰 JSON을 통째로 열지 않고 훑어보기 흐름 도식
큰 JSON 파일을 통째로 메모리에 올리지 않고, 필요한 경로만 스트림으로 확인하는 흐름.

대용량 JSON 응답을 확인할 때 나는 아직도 가끔 습관적으로 jq .부터 친다. 작은 API 응답이면 이게 제일 빠르다. 문제는 파일이 수십 MB를 넘어가거나, 배열 안에 객체가 몇 만 개씩 들어 있는 로그를 볼 때다. 화면은 예쁘게 접히기 전에 이미 느려지고, 내가 실제로 보고 싶었던 필드는 출력 더미 안에 묻힌다. 이때 필요한 건 JSON을 더 예쁘게 여는 일이 아니라, 처음부터 전부 열지 않는 방식이다.

jq의 --stream 옵션은 이럴 때 꽤 실용적이다. 공식 매뉴얼 표현대로라면 입력을 streaming 방식으로 파싱해서, 각 leaf 값과 그 값까지 가는 path를 배열로 내보낸다. 처음 보면 출력 모양이 낯설다. 원래 객체 구조를 그대로 보여 주는 대신 [["items",0,"status"],"ok"] 같은 조각들이 흘러나오기 때문이다. 그런데 큰 JSON을 디버깅할 때는 이 조각 단위가 오히려 편하다. 전체 구조를 머리에 올리지 않고도, 필요한 경로와 값만 먼저 걸러낼 수 있다.

통째 pretty print가 느려지는 순간

내가 --stream을 떠올리는 기준은 단순하다. 첫째, 파일이 커서 jq . 출력 자체가 부담스러울 때다. 둘째, 내가 찾는 값이 몇 개 필드로 이미 좁혀져 있을 때다. 셋째, 실패 원인을 보려는데 응답 전체가 너무 넓어 로그 뷰어에서 스크롤 싸움이 되는 경우다. 이 세 조건 중 둘 이상이면 pretty print는 확인 도구가 아니라 노이즈 생성기가 되기 쉽다.

예를 들어 배치 평가 결과가 아래처럼 큰 배열로 쌓인다고 하자. 나는 전체 row를 다 볼 필요가 없고, 실패한 항목의 id, status, error 정도만 먼저 보고 싶다.

jq --stream '
  select(length == 2)
  | select(.[0][-1] == "id" or .[0][-1] == "status" or .[0][-1] == "error")
' result.json

이 출력은 원래 JSON처럼 예쁘지 않다. 대신 내가 보고 싶은 leaf만 계속 나온다. path의 마지막 키가 status인지, error인지 보는 식으로 필터를 걸면 전체 객체를 매번 복원하지 않아도 된다. 처음부터 완성된 리포트를 만들겠다는 욕심을 버리고, “어느 깊이에 어떤 값들이 분포하는가”를 먼저 보는 용도에 가깝다.

path를 로그의 좌표로 쓰기

--stream 출력에서 제일 중요한 건 값보다 path일 때가 많다. 큰 JSON에서 문제는 “에러가 있다”가 아니라 “어느 배열의 몇 번째 항목, 어느 중첩 필드에서 에러가 반복되는가”다. path가 있으면 이 좌표를 먼저 잡을 수 있다. 예를 들어 ["items",531,"error","code"] 같은 조각이 반복해서 보이면, 원본 구조 전체를 펼치지 않아도 531번째 항목 근처를 다시 열어 보면 된다는 판단이 선다.

나는 이걸 JSON판 ID 기준 diff와 비슷하게 본다. JSONL 비교에서 stable key를 먼저 고정하듯, 큰 JSON에서는 path를 먼저 고정한다. 줄 번호는 pretty print 방식이나 포맷팅에 따라 쉽게 바뀌지만, path는 데이터 구조의 좌표에 더 가깝다. 디버깅 메모에도 “line 8423”보다 “items[531].error.code”라고 적어 두는 쪽이 다음에 다시 열기 쉽다.

바로 리포트로 만들려고 하지 않기

--stream을 쓰면서 내가 자주 저지른 실수는 첫 명령부터 최종 리포트를 만들려는 것이다. path를 group하고, 값을 모으고, 객체로 다시 조립하고, 정렬까지 한 번에 하려 한다. 그러면 명령어가 금방 읽기 어려워진다. 이 옵션의 장점은 복잡한 변환보다 초기 관찰 비용을 낮추는 데 있다고 보는 편이 낫다.

jq -c --stream '
  select(length == 2)
  | {path: .[0], value: .[1]}
' result.json

나는 보통 이 정도로 한 번 얇게 뽑아 본다. 그다음 path 패턴이 보이면 그때 필터를 좁힌다. 특정 키만 남길 수도 있고, 배열 index 범위를 좁힐 수도 있고, 값이 null인 leaf만 볼 수도 있다. 중요한 건 첫 번째 명령이 답을 내는 게 아니라, 다음 명령을 작게 만들 힌트를 주는 것이다. 큰 JSON 디버깅은 한 번에 정답을 뽑는 일보다, 볼 범위를 빨리 줄이는 일이 더 자주 필요했다.

언제 쓰지 않을지

물론 --stream이 항상 좋은 건 아니다. 구조가 작고 사람이 읽어야 하는 응답이면 그냥 jq .가 낫다. 중첩 객체를 그대로 이해해야 하는 문서형 JSON도 stream 조각으로 보면 오히려 맥락이 끊긴다. 또 path와 leaf만 흘러나오기 때문에, 인접 필드들을 한 row처럼 같이 봐야 하는 작업에는 추가 조립이 필요하다. 이때는 처음부터 stream으로 끝까지 밀기보다, stream으로 의심 구간을 찾고 원본 JSON을 다시 좁게 여는 편이 덜 피곤하다.

메모에는 명령어보다 관찰값을 남기기

이 옵션을 쓴 뒤에는 명령어 자체보다 관찰값을 남기는 편이 더 도움이 됐다. “jq --stream 사용”이라고만 적어 두면 다음에 다시 봐도 별 의미가 없다. 대신 “items[*].error.code가 세 종류로 나뉨”, “scores[*].reason은 비었지만 scores[*].raw는 남아 있음”, “실패 row는 500번대 이후에 몰림”처럼 좌표와 패턴을 같이 적는다. 그러면 다음 디버깅은 명령어 암기에서 시작하지 않고, 이미 좁혀 둔 데이터 구조에서 시작한다.

특히 API 응답이나 평가 로그는 며칠 뒤에 다시 열면 처음 본 파일처럼 낯설다. 그때 path 기반 메모가 있으면 같은 JSON을 다시 통째로 펼치지 않아도 된다. 나는 요즘 큰 응답을 볼 때 “값을 찾았는가”보다 “다음에 다시 열 좌표를 남겼는가”를 더 중요하게 본다. 디버깅 속도는 대개 멋진 한 줄 명령어보다, 다음 사람이 같은 파일을 어디서부터 볼지 줄여 놓은 흔적에서 갈린다. 나 혼자 보는 로그라도 이 좌표가 있으면 다음 실행의 비교 기준이 훨씬 선명해진다. 작은 메모지만 재현 비용을 줄이는 효과는 꽤 크다.

내 기준의 사용 순서는 이렇게 정리된다. 먼저 jq .가 부담스러운 크기인지 본다. 그다음 내가 찾는 키가 이미 좁혀져 있는지 확인한다. 둘 다 맞으면 --stream으로 path와 leaf를 훑고, 반복되는 좌표를 잡은 뒤, 마지막에 원본 구조로 돌아와 해당 구간만 읽는다. 이 정도만 해도 큰 응답 앞에서 멍하니 전체를 펼치는 시간이 꽤 줄어든다. 도구를 새로 배우는 느낌보다, JSON을 한 번에 삼키지 않는 작은 습관에 더 가깝다.

댓글

홈으로 돌아가기

검색 결과

"" 검색 결과입니다.