기본 콘텐츠로 건너뛰기

JSON Array Hook: Control Lists

thumbnail

`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/loadsJSONDecoderarray_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

댓글

이 블로그의 인기 게시물

Django에서 트랜잭션 관리하기

Django에서 트랜잭션 관리하기 안녕하세요! 오늘은 Django에서 데이터베이스 트랜잭션을 효과적으로 관리하는 방법에 대해 알아보겠습니다. 1. 트랜잭션의 중요성 트랜잭션은 데이터베이스의 일관성과 무결성을 보장하는 중요한 개념입니다. Django에서는 여러 가지 방법으로 트랜잭션을 관리할 수 있습니다. 1.1 기본 개념 원자성(Atomicity) : 트랜잭션은 모두 실행되거나 모두 실행되지 않아야 합니다. 일관성(Consistency) : 트랜잭션 전후로 데이터베이스의 일관성이 유지되어야 합니다. 격리성(Isolation) : 동시에 실행되는 트랜잭션들이 서로 영향을 주지 않아야 합니다. 지속성(Durability) : 완료된 트랜잭션의 결과는 영구적으로 저장되어야 합니다. 2. Django의 트랜잭션 관리 2.1 기본 설정 # settings.py DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'mydatabase', 'USER': 'myuser', 'PASSWORD': 'mypassword', 'HOST': 'localhost', 'PORT': '5432', 'ATOMIC_REQUESTS': True, # 모든 뷰를 트랜잭션으로 래핑 } } 2.2 데코레이터 사용 from django.db import transaction @transaction.atomic def create_order(user, items): order = Order.objects.create(user=...

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드

AWS S3 + CloudFront로 정적 파일 서빙 완전 가이드 안녕하세요! 오늘은 AWS S3와 CloudFront를 사용하여 정적 파일을 효율적으로 서빙하는 방법에 대해 알아보겠습니다. 왜 S3와 CloudFront를 사용할까요? 높은 가용성 : AWS의 글로벌 인프라를 활용 빠른 전송 속도 : CloudFront의 CDN 기능으로 전 세계 사용자에게 빠른 전송 비용 효율성 : 사용한 만큼만 지불 보안 : AWS의 보안 기능 활용 확장성 : 트래픽 증가에 자동 대응 1. S3 버킷 설정 1.1 버킷 생성 및 설정 import boto3 def create_s3_bucket(): s3 = boto3.client('s3') # 버킷 생성 bucket_name = 'your-static-files-bucket' s3.create_bucket( Bucket=bucket_name, CreateBucketConfiguration={ 'LocationConstraint': 'ap-northeast-2' } ) # 버킷 정책 설정 bucket_policy = { "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObje...

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁

RDS에서 Django 앱 성능을 높이는 데이터베이스 설정 팁 안녕하세요! 오늘은 AWS RDS를 사용하는 Django 애플리케이션의 성능을 최적화하는 방법에 대해 알아보겠습니다. 1. RDS 인스턴스 최적화 1.1 인스턴스 타입 선택 # RDS 인스턴스 크기 조정 import boto3 def resize_rds_instance(): rds = boto3.client('rds') response = rds.modify_db_instance( DBInstanceIdentifier='your-db', DBInstanceClass='db.t3.large', # 워크로드에 맞는 인스턴스 타입 선택 ApplyImmediately=True ) return response['DBInstance'] 1.2 파라미터 그룹 설정 def create_parameter_group(): rds = boto3.client('rds') # PostgreSQL 파라미터 그룹 생성 response = rds.create_db_parameter_group( DBParameterGroupName='django-optimized', DBParameterGroupFamily='postgres13', Description='Optimized parameters for Django applications' ) # 성능 관련 파라미터 설정 parameters = [ { 'ParameterName': 'shared_buffers', 'ParameterValue': '2GB...