![]()
`json.loads()`가 리스트를 그냥 리스트로만 만들던 시대가 끝난다: PR #146441의 `array_hook` 실전 용도
meta_description: CPython PR #146441은 JSON 디코더에 array_hook 파라미터를 추가해, JSON 배열을 파싱할 때 리스트 대신 원하는 타입으로 변환할 수 있게 했다. tuple/커스텀 리스트/검증 래퍼 등으로 일괄 변환하는 방법과, object_hook/object_pairs_hook과의 차이, 운영 코드에서 실수하기 쉬운 포인트(성능/메모리/예외 처리)를 정리한다.
meta_keywords: python, json, loads, JSONDecoder, array_hook, object_hook, object_pairs_hook, decoder, parsing, tuple, list, validation, performance, CPython, practical, 운영, 변환
meta_robots: index,follow
JSON을 오래 만져본 사람은 다 안다.
object_hook는 있다(딕셔너리 후처리)object_pairs_hook도 있다(순서 보존/중복 키 처리)
근데 이상하게도 “배열”에는 훅이 없었다.
그래서 우리는 늘 한 번 더 손을 댔다.
loads()로 일단 파싱하고- 리스트를 전부 걷어내며(재귀)
- tuple로 바꾸거나, 커스텀 컨테이너로 감싸거나, 값 검증을 한다
PR #146441(gh-146440)은 그 빈 칸을 메운다.
JSON 디코더에 array_hook가 들어간다.
이제 “배열이 파싱되는 순간” 리스트를 다른 타입으로 바꾸는 길이 열린다.
![]()
1) 뭐가 추가됐나: load/loads와 JSONDecoder에 array_hook
diff를 보면 핵심은 단순하다.
json.load()/json.loads()에array_hook=인자가 추가된다json.JSONDecoder(..., array_hook=...)가 가능해진다- 파이썬 구현과 C 구현 모두에 반영된다
즉, “문서만”이 아니라 실제 파서 경로에서 지원한다.
이제 기본 사용 형태는 이런 식이 된다.
import json
data = json.loads('[1, 2, 3]', array_hook=tuple)
assert data == (1, 2, 3)
이게 왜 유용하냐면, 후처리 재귀를 안 돌려도 되는 순간들이 있다.
2) object_hook와 뭐가 다르나(헷갈리기 쉬운 포인트)
정리하면 이렇다.
object_hook: JSON object{...}→ dict가 만들어진 다음 후처리object_pairs_hook:{...}의 key/value 쌍 리스트를 받아서 직접 컨테이너를 결정array_hook: JSON array[...]→ list가 만들어진 다음 후처리
즉, array_hook는 “배열 전용 object_hook”이다.
그래서 다음이 가능해진다.
- “모든 배열은 tuple로” 같은 정책
- “모든 배열은 FrozenList처럼 수정 불가능하게” 같은 정책
- “배열 길이가 N 넘으면 예외” 같은 안전장치
3) 실전 용도 1: tuple로 고정해서 ‘의도치 않은 변경’을 막기
운영에서 JSON을 받은 뒤에, 아래 두 문제를 자주 본다.
1) 파싱한 데이터 구조를 여기저기 넘기다가 누군가가 리스트를 append/pop 한다 2) 그 변경이 로그/캐시/서명 검증 결과를 조용히 깨뜨린다
이때 배열을 tuple로 고정하면, 실수가 즉시 터진다(좋은 의미로).
import json
payload = json.loads(body, array_hook=tuple)
# payload 안의 모든 배열은 tuple이 됨
이 패턴은 특히 “외부 입력(JSON)”을 내부 도메인 모델로 빨리 굳히고 싶은 서비스에서 유용하다.
주의
배열을 tuple로 만들면, 이후 코드가 list를 기대하던 곳에서 깨질 수 있다.
- 이건
array_hook의 단점이 아니라 - “원래 애매하게 흘러가던 계약”이 드러나는 것이다
따라서 도입은 점진적으로(특정 endpoint/특정 파서부터) 하는 게 안전하다.
4) 실전 용도 2: 배열 크기/중첩 깊이 제한(DoS/폭주 방어)
JSON은 “구조”로 공격한다.
- 지나치게 깊은 중첩
- 지나치게 큰 배열
이게 Python 레벨에서 메모리와 시간을 잡아먹는다.
array_hook는 여기서 유용한 “마지막 방어선”이 된다.
import json
MAX_LEN = 10_000
def limit_array(a):
if len(a) > MAX_LEN:
raise ValueError(f"array too long: {len(a)}")
return a
obj = json.loads(body, array_hook=limit_array)
이건 완벽한 보안 솔루션은 아니다. 하지만 “후처리로 순회하면서 크기를 재는 비용” 없이, 배열이 만들어질 때마다 바로 잘라낼 수 있다.
5) 실전 용도 3: 리스트 대신 ‘검증 래퍼’를 붙여서 문제 위치를 남기기
개인적으로 제일 마음에 드는 케이스는 이거다.
JSON 검증에서 가장 짜증나는 건 “어디 배열에서 터졌는지” 정보가 약하다는 점이다.
array_hook로 리스트를 감싸면, 나중에 에러가 났을 때 그 배열의 컨텍스트를 붙일 수 있다.
import json
class TaggedList(list):
__slots__ = ("tag",)
def __init__(self, it=(), *, tag=None):
super().__init__(it)
self.tag = tag
def wrap(a):
# 여기서는 tag를 못 박기 어렵지만,
# 최소한 타입을 바꿔서 "여기는 외부 입력"임을 표시할 수 있다.
return TaggedList(a, tag="external-json")
obj = json.loads(body, array_hook=wrap)
이런 식으로 “외부 입력”을 내부 데이터와 구분해두면,
- 후속 처리에서 검증을 빼먹었을 때
- 타입으로라도 경고가 된다
(실무에서는 TaggedList까지는 과한데, 최소한 tuple 변환만 해도 효과가 있다.)
6) 성능/메모리 관점: array_hook는 ‘0원’이 아니다
훅이 들어오면 호출 비용이 생긴다.
그래도 대부분의 서비스에서 병목은 JSON 파싱 자체가 아니라 네트워크/IO/DB인 경우가 많고, array_hook는 “후처리 재귀”를 줄여서 오히려 이득이 될 수 있다.
하지만 아래 케이스는 조심하자.
- 초당 수천~수만 req에서 큰 JSON을 파싱한다
- 배열이 엄청나게 많은 데이터(작은 배열이 수천 개)
이럴 땐 훅 호출이 누적된다.
실무 팁:
array_hook=tuple처럼 가벼운 변환이 1순위다(여기서 무거운 일을 하지 말기)- “검증”은 훅에서 끝내지 말고, 훅은 신호만 남기고(길이/타입), 본 검증은 별도 단계로 빼라
- 정말 빡센 트래픽이면, payload를 전부 강제 변환하는 대신 특정 엔드포인트/특정 요청군에만 켠다
내가 쓰는 계측 포인트(운영용)
array_hook를 넣으면, 체감상 느려졌는지 판단이 어려울 때가 있다. 이때는 훅 자체가 아니라 배열 개수와 총 원소 수를 찍어보면 답이 빨리 나온다.
import json
class C:
arrays = 0
elems = 0
def count_array(a):
C.arrays += 1
C.elems += len(a)
return a # 변환은 하지 않고, 카운트만
json.loads(body, array_hook=count_array)
print(C.arrays, C.elems)
이 값이 “생각보다 큰” 서비스라면, array_hook를 켜는 순간 훅 호출이 폭증한다. 그때는 변환 정책을 최소화하거나, 특정 데이터에만 적용하는 편이 낫다.
7) 언제 써야 하나(내 결론)
array_hook는 “있으면 좋은 옵션” 수준이 아니라, JSON을 도메인 모델로 굳히는 서비스에선 꽤 큰 도구다.
- 외부 입력을 불변(immutable)로 만들고 싶다 →
array_hook=tuple - 폭주 방어를 하고 싶다 → 길이 제한 훅
- 파싱 직후 정책을 강제하고 싶다 → array_hook + object_hook 조합
반대로, 데이터 과학/ETL 쪽에서 “리스트를 바로 numpy로” 같은 걸 기대하면 오히려 실망할 수 있다. (그건 대개 파서 단이 아니라 변환/검증 파이프라인의 문제다.)
Keywords
python,json,decoder,array_hook,loads,load,JSONDecoder,object_hook,object_pairs_hook,tuple,list,parsing,validation,performance,DoS,limits,immutability,hook,pr
References
- CPython PR #146441
- https://github.com/python/cpython/pull/146441
- Diff
- https://github.com/python/cpython/pull/146441.diff
이미지 크레딧/라이선스
- Macro laptop coding (Unsplash).jpg — Marc Mueller / CC0
- https://commons.wikimedia.org/wiki/File:Macro_laptop_coding_(Unsplash).jpg
- http://creativecommons.org/publicdomain/zero/1.0/deed.en
댓글
댓글 쓰기