API 응답으로 받은 JSON 데이터는 계층 구조로 깊게 중첩되어 있는 경우가 많습니다. Python의 json 모듈로 파싱하면 dict 형태가 되지만, 깊은 곳의 값을 꺼내려면 여러 단계의 키 접근이나 반복문이 필요합니다. 특히 필드명이 동적으로 변하거나 조건 필터링이 필요할 때 코드가 복잡해집니다.
문제 상황
다음과 같은 응답 구조에서 특정 데이터를 추출해야 한다고 가정해 봅시다.
payload = {
"status": 200,
"message": "조회 완료",
"result": {
"members": [
{"nickname": "Alpha", "sex": "male", "score": 88},
{"nickname": "Beta", "sex": "female", "score": 92},
{"nickname": "Gamma", "sex": "male", "score": 76},
{"nickname": "Delta", "sex": "female", "score": 95},
],
"items": [
{"label": "keyboard", "cost": 120, "stock": 50},
{"label": "mouse", "cost": 35, "stock": 200},
{"label": "monitor", "cost": 300, "stock": 30},
]
}
}
세 가지 요구사항을 해결해야 합니다:
- 모든 회원의 닉네임 목록 수집
- 필드명이
members에서members_2024,members_567등으로 동적 변경될 때 대응 - 점수가 90 이상인 여성 회원만 필터링
JSONPath 소개
JSONPath는 JSON 데이터를 XPath 스타일로 탐색하기 위한 쿼리 언어입니다. 복잡한 중첩 구조와 조건 필터링을 간결하게 처리할 수 있습니다.
설치
pip install jsonpath
핵심 문법
| 표현식 | 의미 |
|---|---|
$ | 루트 노드 |
. | 직계 자식 접근 |
.. | 모든 하위 노드 재귀 탐색 |
* | 임의의 노드 매칭 |
[n] | 인덱스 접근 |
[start:end] | 슬라이싱 |
[?(@.field op value)] | 조건 필터 |
실전 적용 예제
from jsonpath import jsonpath
payload = {
"status": 200,
"message": "조회 완료",
"result": {
"members": [
{"nickname": "Alpha", "sex": "male", "score": 88},
{"nickname": "Beta", "sex": "female", "score": 92},
{"nickname": "Gamma", "sex": "male", "score": 76},
{"nickname": "Delta", "sex": "female", "score": 95},
],
"items": [
{"label": "keyboard", "cost": 120, "stock": 50},
{"label": "mouse", "cost": 35, "stock": 200},
{"label": "monitor", "cost": 300, "stock": 30},
]
}
}
# 1. 모든 닉네임 추출
names = jsonpath(payload, "$.result.members[*].nickname")
print(names) # ['Alpha', 'Beta', 'Gamma', 'Delta']
# 2. 동적 필드명 대응 - result 하위 임의 필드의 모든 name 계열 키값
dynamic_names = jsonpath(payload, "$.result.*[*].nickname")
print(dynamic_names)
# 3. 조건 기반 필터링: score 90 이상이면서 female인 회원
filtered = jsonpath(
payload,
"$.result.members[?(@.score >= 90 && @.sex == 'female')]"
)
print(filtered)
# [{'nickname': 'Beta', 'sex': 'female', 'score': 92},
# {'nickname': 'Delta', 'sex': 'female', 'score': 95}]
# 4. 재귀 탐색으로 모든 label 값 수집 (items 내부)
labels = jsonpath(payload, "$..label")
print(labels) # ['keyboard', 'mouse', 'monitor']
# 5. 슬라이싱: 첫 번째와 두 번째 데이터만
top_two = jsonpath(payload, "$.result.members[0:2]")
print(top_two)
# 6. 특정 인덱스 직접 접근
third = jsonpath(payload, "$.result.members[2]")
print(third) # [{'nickname': 'Gamma', 'sex': 'male', 'score': 76}]
고급 활용 패턴
다중 조건 및 비교 연산
# 재고가 100개 미만이면서 가격이 100 초과인 상품
premium_low_stock = jsonpath(
payload,
"$.result.items[?(@.cost > 100 && @.stock < 100)]"
)
부모 참조 없이 전역 검색
# 문서 전체에서 nickname 필드를 가진 객체 검색
all_users = jsonpath(payload, "$..[?(@.nickname)]")
결과가 없을 때의 처리
# 매칭 결과 없으면 False 반환, 주의 필요
result = jsonpath(payload, "$.nonexistent.path")
# result == False
# 안전하게 사용하려면
def safe_jsonpath(data, expr, default=None):
res = jsonpath(data, expr)
return res if res is not False else default
주의사항
jsonpath()함수는 결과가 없으면False를 반환하므로, 리스트 연산이나 조건 분기 시 주의가 필요합니다.- 복잡한 논리 연산은 라이브러리 버전에 따라 지원 범위가 다를 수 있어 공식 문서 확인이 권장됩니다.
- 대용량 JSON의 경우 과도한 재귀 탐색(
..)은 성능 저하를 유발할 수 있습니다.