JSON vs YAML vs TOML: 어떤 설정 형식을 사용할까
가장 일반적인 세 가지 설정 형식의 실용적 비교와 사용 사례.
설정 형식은 파서가 no를 조용히 false로 바꾸거나, 64비트 ID를 잘라내거나, 설정 파일에서 임의의 객체를 생성하기 전까지는 서로 대체 가능해 보입니다. JSON, YAML, TOML이 현대 툴링을 지배하고 있으며, 팀이 겪는 고통의 대부분은 각 형식이 정확히 어디에서 휘어지는지 모르는 데서 옵니다. 이 글은 취향 비교가 아니라 메커니즘 수준의 비교입니다.
세 개의 사양, 세 가지 철학
JSON은 RFC 8259와 ECMA-404로 정의됩니다. 여섯 가지 값 타입, 주석 없음, 냅킨에 다 적을 수 있는 문법. YAML 1.2.2(2021)는 세 개의 표현 계층, 태그, 앵커, 멀티 문서 스트림을 설명하는 약 100페이지짜리 문서입니다. TOML 1.0.0(2021)은 그 사이에 있습니다. 해시 테이블에 모호함 없이 매핑되도록 명시적으로 설계된, 타입이 있고 주석 친화적인 형식입니다.
사양의 크기는 실무에서 중요합니다. 주류 JSON 파서는 모두 본질적으로 동일하게 동작합니다. 반면 두 YAML 파서는 같은 파일에 대해 정당하게 서로 다른 결과를 낼 수 있습니다. PyYAML을 포함한 많은 라이브러리가 여전히 YAML 1.1 의미론을 구현하고 있고, 다른 라이브러리는 1.2를 구현하기 때문입니다.
JSON: 예측 가능하지만 숫자에 날카로운 모서리
{
"port": 8080,
"debug": true,
"hosts": ["a.example.com", "b.example.com"]
}
JSON의 진짜 위험은 숫자와 중복 키에 숨어 있습니다.
- 숫자 정밀도. RFC 8259는 정밀도 요구사항을 두지 않지만, JavaScript는 모든 숫자를 IEEE 754 배정밀도로 파싱합니다. 2^53 - 1을 넘는 정수는 조용히 자릿수를 잃습니다. Twitter API가
id와id_str을 함께 제공하는 이유가 바로 이것입니다. 64비트 식별자는 문자열로 직렬화하세요. - 중복 키. 사양은 이름이 고유해야 한다(SHOULD)고만 말합니다. 대부분의 파서는 마지막 항목을, 일부는 첫 항목을 유지하며, 검증기와 실행기가 같은 문서를 다르게 파싱할 때 이 불일치가 실제 보안 우회에 악용된 사례가 있습니다.
- 주석 없음은 의도된 설계. Douglas Crockford는 주석 기반 파서 지시문을 막기 위해 주석을 제거했습니다. 손으로 편집하는 설정에는 JSONC(
tsconfig.json의 실제 형식)나 JSON5를 쓰되, 어느 쪽을 허용하는지 툴링에 명시하세요. 순수 JSON 파서는 둘 다 거부합니다.
JSON은 API 페이로드, 락파일, 사람보다 기계가 더 자주 쓰는 모든 것에 사용하세요.
YAML: 읽기 좋지만 1.1 모양의 함정문
port: 8080
debug: true
hosts:
- a.example.com
- b.example.com
YAML 1.2는 no, yes, on, off를 일반 문자열로 만들었지만, YAML 1.1 파서는 이를 불리언으로 강제 변환합니다. 국가 코드 NO가 false가 되는 유명한 Norway 문제입니다. 1.1 규칙은 1:30을 60진법 정수 90으로 파싱하고 버전 번호 3.10을 부동소수점 3.1로 바꿉니다. 어떤 파서가 파일을 읽을지 통제할 수 없는 경우가 많으므로, 운영 규칙은 이것입니다. 다른 타입의 스칼라처럼 보일 수 있는 모든 문자열에 따옴표를 붙이세요.
두 가지 메커니즘을 더 존중해야 합니다.
- 앵커와 별칭(
&base/*base)은 설정 중복을 깔끔하게 제거하지만 billion-laughs 공격을 가능하게 합니다. 몇 개의 중첩 별칭이 기가바이트로 팽창합니다. 신뢰할 수 없는 YAML은 별칭 제한을 걸거나 별칭 자체를 거부하고 파싱하세요. - 임의 객체 생성. PyYAML의 레거시
yaml.load()는 태그된 노드에서 임의의 Python 클래스를 인스턴스화할 수 있었습니다. 항상safe_load또는 기본적으로 안전한 파서를 사용하세요.
들여쓰기에 탭은 금지되며, 공백 하나가 중첩을 조용히 바꿀 수 있다는 점도 기억하세요. 생태계가 YAML을 강제한다면(Kubernetes, GitHub Actions, Ansible) 스키마 검증과 짝지으세요. yamllint에 schemastore.org의 JSON Schema를 더하면 대부분의 재앙을 프로덕션 도달 전 CI에서 잡아냅니다.
TOML: 의도적으로 지루함
port = 8080
debug = true
hosts = ["a.example.com", "b.example.com"]
[database]
url = "postgres://localhost:5432/app"
[[server]]
name = "eu-1"
[[server]]
name = "us-1"
TOML의 설계 목표는 구체적인 보장으로 나타납니다.
- 모든 값이 명시적 타입을 가집니다. 문자열, 정수, 부동소수점, 불리언, 배열, 테이블, 그리고 RFC 3339 날짜시간. 날짜가 1급 시민이라 문자열 관례가 필요 없습니다.
- 중복 키는 구현 선택이 아니라 하드 파싱 에러입니다.
- 들여쓰기에 의미가 없습니다. 구조는
[table]헤더와 점 표기 키에서 나오므로, 재포맷이 의미를 바꿀 수 없습니다.
비용은 중첩의 인체공학입니다. 깊이 중첩된 테이블 배열은 금세 장황해집니다. Cargo와 pyproject.toml이 TOML에서 훌륭하게 느껴지는 반면 Kubernetes 매니페스트는 그렇지 않을 이유입니다. 유용한 휴리스틱: 설정이 3단계 이상 중첩된다면 문제는 형식이 아니라 설정 스키마입니다.
실무에서의 선택
- 기계 간 데이터, API, 락파일: JSON
- 사람이 편집하는 애플리케이션·도구 설정: TOML
- 생태계가 강제하는 인프라 설정: YAML, 반드시 스키마 검증과 모호한 스칼라 따옴표 처리와 함께
- 주석이 필요하지만 JSON 호환이어야 하는 설정: JSONC/JSON5, 명시적으로 문서화
반복되는 실수들
- 64비트 ID를 JSON 숫자로 내보내고 몇 주 뒤 데이터 손상을 디버깅
- 따옴표 없는 YAML 값: 국가 코드, 우연히 모두 숫자인 git SHA,
3.10같은 버전 - 별칭·깊이 제한 없이 사용자로부터 YAML을 수용
- 모든 파서가 중복 키를 같은 방식으로 처리한다고 가정
- 직렬화기 대신 문자열 연결로 이 형식들을 손으로 생성
형식 간 변환
세 형식 모두 교집합에서 같은 데이터 모델(맵, 배열, 스칼라)을 공유하므로, 기계적 변환은 보통 한 방향으로만 무손실입니다. JSON에서 YAML로는 항상 작동합니다. YAML에서 JSON으로는 앵커, 멀티 문서, 문자열이 아닌 키에서 실패합니다. TOML에서 JSON으로는 주석 계층이 사라지는데, 이는 종종 파일에서 가장 가치 있는 내용입니다. 사람이 편집하는 형식을 진실의 원천으로 삼고, 기계용 형식은 CI에서 생성하며, 왕복 변환은 절대 하지 마세요. 한 방향으로만 변환하고, 두 산출물을 모두 커밋하고, 리뷰에서 디프를 확인하세요.
커밋 전에 검증하세요. sdk.is/json-formatter의 JSON 포맷터나 sdk.is/yaml-validator의 YAML 검증기에 설정을 붙여 넣고, 문제를 파서가 먼저 찾게 하세요.