JSONPath 쿼리 언어 실전
핵심 요약: 2024년 JSONPath는 마침내 공식 표준 RFC 9535를 얻었습니다. 이 글은 문법, 필터 의미, 함수, 구현 간 역사적 차이를 설명합니다.

핵심부터 말하면, 먼저 명시적 경로로 찾고 그 위에 필터를 얹으며, RFC 9535에 맞춰 표현식을 쓰세요. 고정 필드 하나에는 속성 접근이나 JSON Pointer를, 집계에는 jq나 코드를 쓰세요.
$.store.book[?(@.price < 10)].title같은 표현식을 아마 본 적이 있을 것입니다. 그러나 국제 표준이 된 것은 2024년입니다.
1. 사실상 표준에서 공식 표준으로
JSONPath는 2007년 Stefan Gössner가 제안했고, 오랫동안 유일한 "명세"는 블로그 글 하나였으며 각 언어 구현이 자기 방식대로 했습니다:
- 필터에 괄호가 필요한지(
?(@.a)대?@.a) - 배열 인덱스
[-1]을 지원하는지 - 결과가 "값"을 반환하는지 "값과 경로"를 반환하는지
- 스크립트 표현식(
?(@.price < $.max))을 허용하는지
2024년 2월에 발표된 RFC 9535가 이런 분기를 단일 문법과 의미로 수렴시키고 공식 적합성 테스트 스위트를 제공했습니다. 신규 프로젝트는 RFC 9535를 따르고, 옛 프로젝트를 이전할 때는 먼저 구현 버전을 확인하세요.
2. 문법 치트시트
| 문법 | 의미 | 예 |
|---|---|---|
$ |
루트 노드 | $.store |
@ |
필터 안의 현재 노드 | $[?@.price < 10] |
.name |
자식 노드(이름이 [A-Za-z_][A-Za-z0-9_]*와 일치해야 함) |
$.store.book |
['name'] |
자식 노드(비 ASCII와 점 포함 아무 이름) | $['store']['书'] |
[0] |
배열 인덱스, 음수는 끝에서 셈 | $.book[0], $.book[-1] |
[0,2] |
여러 인덱스 / 여러 이름 | $.book[0,2] |
[1:3] |
슬라이스, 시작 포함 끝 배타 | $.book[1:3] |
[::2] |
간격이 있는 슬라이스 | $.book[::2] |
[*] |
모든 자식 노드 와일드카드 | $.book[*].title |
..name |
재귀 하강, 어느 깊이에서든 검색 | $..price |
..[*] |
모든 노드를 재귀적으로 취함 | $..* |
[?<expr>] |
필터 선택자 | $.book[?@.price < 10] |
반드시 기억할 의미 하나: JSONPath는 단일 값이 아니라 노드 집합(nodelist)을 반환합니다. 노드가 하나만 매칭되어도 결과는 길이 1의 목록입니다.
3. 샘플 하나로 문법 훑기
{
"store": {
"book": [
{ "title": "JSON 权威指南", "price": 8.95, "tags": ["json", "spec"] },
{ "title": "数据交换格式史", "price": 12.99, "tags": ["history"] },
{ "title": "解析器实现原理", "price": 8.99, "tags": ["parser", "json"] }
],
"bicycle": { "color": "red", "price": 19.95 }
},
"expensive": 10
}
| 표현식 | 결과 |
|---|---|
$.store.bicycle.color |
["red"] |
$..price |
가격 4개(자전거 가격 포함) |
$.store.book[*].title |
제목 3개 |
$.store.book[?@.price < 10].title |
1번과 3번 책의 제목 |
$.store.book[?@.tags] |
tags가 있는 모든 책(존재 검사) |
$.store.book[-1].title |
마지막 책의 제목 |
$..book[0,1] |
처음 두 책 |
$.store.book[?@.price < $.expensive] |
문법상 구현 의존, RFC 9535는 교차 루트 참조를 허용하지 않음 |
마지막 행은 역사적 함정입니다. 많은 구현이 필터 안에서 $.expensive로 교차 노드 비교를 허용하지만 RFC 9535는 명시적으로 금지합니다.
필터는 현재 노드 @만 접근할 수 있습니다. 전역 임계값과 비교해야 한다면 임계값을 쿼리 함수의 인자로 넘기세요.
4. 필터가 표현할 수 있는 것
RFC 9535 필터 표현식은 네 종류의 구성을 지원합니다:
Logic: && || ! e.g. ?@.price < 10 && @.tags
Comparison: == != < <= > >=
Existence: a bare path e.g. ?@.tags (tests existence)
Functions: length() count() match() search() value()
실용 예 몇 가지:
// Price between 8 and 10
$.store.book[?@.price >= 8 && @.price <= 10]
// Title contains "JSON" (regex match; the second argument is an I-Regexp)
$.store.book[?match(@.title, ".*JSON.*")]
// More than one tag
$.store.book[?length(@.tags) > 1]
// Get the first matched value instead of a list (value() turns a single-node set into a value)
$.store.book[?@.price < 9].title.value()
match()는 전체 문자열 매칭이고 search()는 부분 문자열 검색입니다. 바탕 정규식 방언은 I-Regexp(RFC 9485의 부분집합)로,
역참조와 룩어라운드를 지원하지 않습니다. 제한이자 정규식 DoS에 대한 편리한 방패입니다.
5. 구현 차이: 이전 전에 확인
| 구현 | 언어 | RFC 9535 지원 | 비고 |
|---|---|---|---|
jsonpath-plus |
JavaScript | 대체로 호환 | 확장 함수와 스크립트 표현식 지원, 기능이 많지만 엄격한 표준은 아님 |
jsonpath(dchester) |
JavaScript | 부분 | 오래된 라이브러리, ?() 문법과 다른 결과 구조 |
ojg / gjson |
Go | ojg는 양호 |
gjson은 다른 경로 문법이며 JSONPath가 아님 |
jsonpath-ng |
Python | 부분 | 확장 지원, 상당히 관대 |
Jayway JsonPath |
Java | 부분 | 생태계가 넓고 역사적 동작이 RFC와 다름 |
이전 전 세 가지 확인:
- 필터 괄호가 필요한지
-1같은 음수 인덱스를 지원하는지- 결과가 "값 배열"인지 "
{path, value}객체 배열"인지
세 번째가 가장 넘어지기 쉽습니다. 프런트엔드가 result[0]을 읽는데 라이브러리를 바꾼 뒤 객체를 받아 페이지가 빈 화면이 됩니다.
6. 성능과 보안
성능
- JSONPath는 본질적으로 단일 순회이고 O(노드 수) 복잡도이며 인덱스를 쓸 수 없습니다.
..를 쓰는 재귀 하강이 가장 비쌉니다.$.store.book으로 먼저 찾고 필터할 수 있으면$..book을 쓰지 마세요.- 표현식을 한 번 컴파일해 재사용하세요(대부분 라이브러리가
compile이나 캐싱 제공). 질의마다 재파싱하지 마세요. - 노드가 10만 개를 넘는 문서는 JSON Pointer로 정확히 추출하거나 서버에서
jq로 전처리하는 것을 고려하세요.
보안
- 사용자 입력 표현식은 길이와 중첩 깊이에 제한이 있어야 합니다.
- JSONPath에 권한 부여를 맡기지 마세요. 읽기만 할 수 있고 "어떤 필드가 보이지 않는지"를 표현할 수 없습니다.
match()가 쓰는 정규식이 사용자에게서 오면 복잡도를 제한하세요. PCRE보다 안전하지만 길이 상한은 여전히 권장합니다.
7. JSONPath를 쓰지 말아야 할 때
- 고정 필드 하나만 필요 →
data.store.bicycle.color를 직접 쓰세요. - 수정 후 다시 써야 함 → JSON Pointer나 패치를 쓰세요. JSONPath는 다시 쓰지 않습니다.
- 집계(합계, 그룹화)가 필요 →
jq, SQL, 또는 코드를 쓰세요. JSONPath는 선택만 합니다. - 문서가 거대하고 몇 필드만 필요 → 전체 트리를 메모리에 먼저 올리지 말고 스트리밍 파서를 쓰세요.
8. 체크리스트
- [ ] 표현식이 RFC 9535를 따르고 교차 루트 참조에 의존하지 않습니다
- [ ] 결과가 노드 집합임을 명확히 알고
[0]이나value()로 단일 값을 얻습니다 - [ ] 표현식이 미리 컴파일되어 재사용됩니다
- [ ] 사용자 제어 표현식에 길이와 복잡도 제한이 있습니다
- [ ] 구현을 바꿀 때 공식 적합성 테스트 사례를 실행합니다
FAQ
JSON Pointer와 JSONPath 중 어떻게 고르나?
고정 필드 하나만 필요하면 평범한 속성 접근이나 JSON Pointer를 쓰고, 필터, 와일드카드, 재귀 하강이 필요하면 JSONPath를 꺼내세요. JSONPath는 선택만 하고 다시 쓰지 않으므로 쓰기는 JSON Pointer나 패치의 몫입니다.
필터가 왜 $.expensive를 참조할 수 없나?
RFC 9535는 교차 루트 참조를 명시적으로 금지합니다. 필터는 현재 노드 @만 접근할 수 있습니다. 대신 임계값을 쿼리 함수의 인자로 넘기세요. 옛 구현은 문법을 받아들이지만 라이브러리를 바꾸거나 업그레이드하면 깨집니다.
result[0]으로 값을 읽는 데 의존해도 되나?
현재 구현이 값 배열을 반환함을 확인한 뒤에만 하세요. 일부 라이브러리는 {path, value} 객체를 반환해 result[0]이 객체가 되고 페이지가 빈 화면이 됩니다. 단일 값은 [0]이나 value()로 읽고, 구현을 바꾸기 전에 공식 적합성 스위트를 실행하세요.