API가 계속 바뀔 때: 동적 JSON 파싱(Dynamic JSON Parsing)과의 사투
요약
API 필드 이름이나 데이터 타입이 예고 없이 변경되는 상황을 해결하기 위한 동적 JSON 파싱 전략을 다룹니다. 수동 조건문이나 Pydantic 모델의 한계를 극복하기 위해 재귀적 탐색과 유사도 기반 매핑을 활용한 '최선의 노력(best effort)' 방식의 파서를 제안합니다.
핵심 포인트
- API 필드명 변경 및 타입 불일치로 인한 파싱 오류 문제 해결
- 수동 if-else 조건문과 Pydantic 모델의 확장성 한계 지적
- 구조적·타입 드리프트 대응을 위한 재귀적 파싱 전략 도입
- 유사도 기반 매핑을 통한 동적 스키마 일치 기법 활용
저는 공개 API로부터 데이터를 수집하는 사이드 프로젝트를 진행해 왔습니다. 다들 아시다시피, 몇몇 엔드포인트(endpoints)를 입력하고, 출력을 정규화(normalize)한 다음, 대시보드에 표시하는 방식이죠. 간단해 보이죠, 그렇죠?
API가 예고 없이 필드 이름을 바꾸기로 결정하기 전까지는 말입니다.
버전 관리되는 파괴적 변경(breaking changes)을 말하는 게 아닙니다. 같은 엔드포인트가 어느 날은 user_name을 반환하고, 다음 날은 username을 반환하는 상황을 말하는 겁니다. 때로는 email이 존재하다가도, 때로는 누락되기도 합니다. 중첩된 객체(nested objects)는 또 어떻고요? 말도 마세요. 중첩된 address 블록이 어떤 때는 문자열(string)로 나타나고, 어떤 때는 객체(object)로 나타나기도 합니다. 이 외부 서비스는 여전히 반복적인 개선(iterating)을 진행 중인 스타트업에서 구축한 것이라, 안정적인 릴리스 주기(release cycle)가 없었습니다.
저의 초기 반응은 순전한 부정이었죠. "그냥 빠르게 파서(parser)를 작성하고 넘어가면 돼."
내가 처음에 시도했던 것 (그리고 왜 실패했는가)
저의 첫 번째 시도는 폴백(fallbacks)을 사용하여 필드를 추출하려고 시도하는 일련의 수동 Python 함수들이었습니다:
def get_user_email(data):
if 'email' in data:
return data['email']
...
저는 이러한 조건부 체인(conditional chains)을 점점 더 많이 추가했습니다. 곧 최상위 필드(top-level fields)만 처리하는 if-else 블록으로 가득 찬 300줄짜리 파일이 생겼습니다. API가 바뀔 때마다 저는 세네 군데의 서로 다른 곳을 업데이트해야 했습니다. 중첩된 데이터는요? 꿈도 꾸지 마세요. address 키의 변경은 대여섯 개의 함수로 연쇄적으로 영향을 미쳤습니다.
그다음에는 추가 필드를 allow로 설정한 pydantic 모델을 사용해 보았습니다. 이는 누락된 필드에는 효과가 있었지만, 동일한 필드의 타입(type)이 바뀌거나 키(key)의 이름이 변경될 때는 도움이 되지 않았습니다. 결국 전체 파싱을 중단시키는 검증 오류(validation errors)가 발생했습니다. 스타트업의 누군가가 phone을 phone_number로 이름을 바꾸는 바람에 운영 환경(Production)이 다운되기도 했습니다.
저에게는 더 똑똑한 무언가가 필요했습니다. 매주 코드를 다시 작성하지 않고도 적응할 수 있는 무언가가 말이죠.
실제로 효과가 있었던 접근 방식: 기본 전략을 활용한 동적 재귀 파싱 (Dynamic, Recursive Parsing with Default Strategies)
핵심 통찰: 구조적 드리프트(Structural Drift, 키가 이동하거나 이름이 변경됨)와 타입 드리프트(Type Drift, 문자열 vs 객체 vs 리스트)를 모두 처리할 수 있는 파서(Parser)가 필요했습니다. 수동 스키마 매핑(Manual schema mapping)은 확장성이 없었습니다. 그래서 저는 "최선의 노력(best effort)" 전략을 사용하는 작은 Python 재귀 파서를 구축했습니다. 이 방식은 여러 알려진 패턴을 시도하며, 일부 데이터가 손실되더라도 일관된 형태(shape)를 반환합니다.
핵심 아이디어는 다음과 같습니다: 원하는 출력에 대한 **대상 스키마(target schema)**를 정의한 다음, 원본 API 응답을 탐색하며 유사도(Levenshtein distance, 일반적인 유의어, 정확한 일치)를 통해 스키마의 키와 일치하는 필드를 찾는 함수를 작성하는 것입니다. 각 필드에 대해 값을 추출하려고 시도하며, 만약 중첩된 객체(nested object)라면 해당 중첩 스키마를 사용하여 재귀(recurse)합니다.
import json
from difflib import get_close_matches
...
이를 사용하기 위해, 저는 다음과 같이 대상 스키마를 정의합니다:
user_schema = {
'name': str,
'email': str,
...
여기서 제가 발견한 흥미로운 AI 도구가 유용하게 쓰였습니다. 이 재귀 파서를 제작하면서, 수십 개의 엔드포인트(endpoint)를 위해 이 스키마들을 일일이 손으로 작성하는 것이 매우 지루하다는 것을 깨달았습니다. 그래서 AI 코드 생성기—구체적으로 ai.interwestinfo.com에서 호스팅되는 도구—를 사용하여 샘플 API 응답으로부터 초기 스키마를 생성하는 실험을 했습니다. 몇 개의 원본 JSON 응답을 입력하면, str, int, dict 플레이스홀더(placeholder)가 포함된 시작 스키마를 반환해 주었습니다. 그 후 저는 퍼지 임계값(fuzzy threshold)을 수동으로 조정하고 유의어를 추가했습니다.
AI가 완벽하지는 않았습니다. 때때로 선택적 필드(optional field)를 필수 필드(required)로 오해하거나, 타입 유니온(type unions)을 무시하기도 했습니다. 하지만 보일러플레이트(boilerplate)를 타이핑하는 데 드는 시간을 몇 시간이나 아껴주었습니다. 진짜 승리는 재귀 파싱(recursive parsing) 기술 그 자체였으며, 이제 이 기술은 시스템을 중단시키지 않고 기이한 변형들의 95%를 처리합니다.
내가 배운 것 (그리고 다르게 했을 점)
트레이드오프(Trade-offs):
- 퍼지 매칭 (Fuzzy matching)은 거대한 데이터셋에서 느립니다 (매 파싱마다 O(n * m)). 제 10,000개의 레코드에는 괜찮지만, 수백만 개의 레코드를 다룬다면 매핑 (mapping)을 미리 계산해 두어야 할 것입니다.
- 정밀도 손실: 만약 API가 우편번호를 정수 (integer)로 저장하기로 결정한다면, 제 파서 (parser)는 앞자리의 0을 잃어버릴 것입니다. 그래서 문자열 (string)을 위한 특별한 케이스를 추가했습니다.
- AI가 생성한 스키마 (schema)는 수동 튜닝 (manual tuning)이 필요했습니다. 프로덕션 (production) 환경에서 이를 맹목적으로 신뢰하지는 않을 것입니다.
내가 바꿨을 점: 직접 만든 퍼지 추출기 (fuzzy extractor) 대신, json-schema-validator와 json-diff를 결합한 라이브러리를 사용했을 것입니다. 하지만 빠르고 간단하게 끝내야 하는 사이드 프로젝트 (side project)로서는 이 방식이 효과적이었습니다.
팀을 위한 더 나은 옵션: 만약 API를 직접 제어할 수 있다면, 엄격한 검증 (validation)을 포함한 OpenAPI 스펙 (spec)을 강제하십시오. 이 모든 혼란은 불안정한 API의 증상입니다. 하지만 때로는 다른 사람이 만든 혼돈을 그대로 물려받기도 합니다.
마치며
저는 여전히 예측 불가능한 API를 싫어합니다. 하지만 이제는 업스트림 (upstream) 팀이 제멋대로 행동할 때도 더 편하게 잠들 수 있게 해주는 도구(단순한 AI 어시스턴트가 아닌 기술적 방법론)를 갖게 되었습니다. 폴백 전략 (fallback strategies)을 갖춘 재귀적 파서 (recursive parser)는 제 주말을 여러 번 구해 주었습니다.
여러분은 끊임없이 변하는 제3자 데이터 (third-party data)를 어떻게 처리하시나요? 비슷한 접근 방식을 시도해 보셨나요, 아니면 그냥 거대한 try-except 문을 작성하시나요? 댓글로 알려주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기