JSON이냐 YAML이냐 TOML이냐
이 세 형식의 차이는 무엇을 표현할 수 있느냐보다 어떻게 깨지느냐에 있고, 여러분의 시간을 가져가는 쪽은 깨지는 방식입니다.
이 페이지의 모든 주장은 직접 측정했거나 출처가 있습니다. 둘 다 아닌 경우에는 그렇다고 명시합니다.
배포 파이프라인이 YAML 파일에서 국가 목록을 읽습니다. 누군가 노르웨이를 ISO 코드 NO로 추가하자, 파이프라인이 그 시장을 건너뛰기 시작하는데 어디에도 오류는 없습니다. 애플리케이션에 도착한 값은 불리언 false였습니다.
어떤 형식을 고를지 결정해야 하는 것은 이런 종류의 일이지, “주석 지원: 예/아니오” 같은 표가 아닙니다. 세 형식 모두 문자열에서 값으로 가는 맵을 담을 수 있습니다. 셋을 가르는 것은, 아무도 보지 않을 때 여러분에게 무슨 짓을 하느냐입니다.
JSON: 지루하고, 바로 그게 핵심
JSON은 전송용 형식입니다. 타입은 여섯, 허용되는 공백 문자는 넷(RFC 8259에 따라 스페이스, 탭, 캐리지 리턴, 라인 피드), 주석 없음, 끝 쉼표 없음, 날짜 타입 없음, 그리고 어떤 읽는 쪽이든 float64로 해석해도 되는 숫자 타입 딱 하나. 중요한 몇 군데에서 명세가 부족한데, 특히 중복 키가 그렇습니다. RFC는 키가 고유해야 한다(SHOULD)고만 말하고 동작은 정의하지 않은 채 둡니다. JavaScript와 파이썬은 둘 다 마지막 것을 취합니다.
JSON의 미덕은 전적으로 기술 바깥에 있습니다. 모든 언어가 표준 라이브러리에 파서를 넣어 배포합니다. 모든 HTTP 클라이언트가 다룰 줄 압니다. 버전 어긋남은 사실상 없습니다. 2008년에 쓰인 JSON 문서는 오늘도 어디서나 똑같이 파싱됩니다. 네트워크 한 홉, 로그 한 줄, 메시지 큐, 캐시를 위해 직렬화할 때 다른 두 형식의 사람 친화적 기능은 아무것도 사 주지 않고, 보편성은 많은 것을 사 줍니다.
실패의 형태는 잘 다져져 있고 대부분 숫자 이야기입니다. Number.MAX_SAFE_INTEGER는 9007199254740991이고 그보다 큰 ID는 조용히 다시 쓰입니다. 이건 그 자체로 한 편의 글입니다. 날짜는 관행상 문자열이고 그 관행을 강제하는 것은 아무것도 없습니다. 이것도 그 자체로 한 편의 글입니다. 둘 다 전송용으로 다른 형식을 고를 이유는 못 됩니다. 조심할 이유일 뿐입니다.
YAML: 진짜 편의성, 진짜 청구서
사람들이 YAML을 고르는 이유는 우아해서가 아닙니다. 쿠버네티스 매니페스트나 CI 파이프라인은 사람이 매일 손으로 고치는 물건이고, JSON은 손으로 고치기에 정말로 불쾌하기 때문입니다. 주석은 못 쓰고, 따옴표는 의무이고, 빠진 쉼표는 400줄 위에 있습니다. YAML은 주석을 주고, 읽히는 여러 줄 문자열을 주고, 구두점 잡음을 없앱니다. 그건 값어치가 있습니다.
그 대가는 이렇습니다.
노르웨이 문제
YAML 1.1은 따옴표 없는 no, yes, on, off, y, n을 불리언으로 해석합니다. YAML 1.2의 core 스키마는 그렇게 하지 않고 문자열로 둡니다. 같은 문서, 같은 키, 두 개의 답:
a: no
YAML 1.2 core 해석에서 그 값은 문자열 "no"입니다. 1.1 규칙에서는 불리언 false입니다. 어느 쪽이 되느냐는 파일이 아니라 라이브러리에 달렸습니다. PyYAML과 루비의 Psych는 1.1 규칙으로 해석하고, js-yaml은 1.2를 따릅니다. Go의 yaml.v3는 중간에 있어, 대상 필드가 타입이 지정된 bool이 아니면 no를 문자열로 해석하지만, bool이면 1.1식 표기도 받아들입니다. 같은 설정 파일을 읽는 파이썬 서비스와 Node 서비스가 값을 두고 어긋나고, 어느 쪽도 아무것도 기록하지 않습니다.
해결책은 다른 무언가로 오해될 수 있는 문자열을 전부 따옴표로 감싸는 것입니다. 국가 코드, 버전 번호(1.10은 실수고 "1.10"은 아닙니다), 0으로 시작하는 것 전부, 그리고 사용자가 넣는 모든 값. YAML을 프로그램으로 생성한다면, 자기 리뷰를 믿지 말고 출력기가 방어적으로 따옴표를 붙이게 하세요.
공백이 문법이고 탭은 불법이다
들여쓰기가 구조를 담으므로, 어긋난 줄은 오류가 아니라 다른 문서가 됩니다. 더 나쁘게도, YAML 명세는 들여쓰기에 탭 문자를 쓰는 것을 대놓고 금지합니다. 탭을 넣도록 설정된 에디터는, 터미널에서는 보이지도 않는 문자에 대한 메시지와 함께 파싱에 실패하는 파일을 만들어 냅니다. 에디터를 파일 종류별로 설정해 두고 이 문제는 잊으세요.
앵커는 나가는 길에 펼쳐진다
앵커와 별칭은 블록을 한 번 정의해 재사용하게 해 줍니다.
defaults: &defaults
timeout: 30
retries: 3
staging:
<<: *defaults
host: stage.internal
거의 똑같은 서비스 정의 마흔 개를 관리하는 사람에게 YAML을 팔아 주는 기능이 이것입니다. 동시에 데이터 모델에는 없는 기능이기도 합니다. 그 파일을 JSON으로 바꾸면 병합 키는 해석되고, 별칭은 펼쳐지고, defaults가 통째로 staging 안에 나타납니다. 다시 YAML로 되돌리면 글자 그대로의 사본 두 개를 얻습니다. 엄밀히 말해 틀린 건 없지만, 여러분이 관리하던 그것은 사라졌습니다. 앵커에 기대는 YAML 파일은 사실 변환 가능한 것이 아니라 한 번 읽을 수 있을 뿐입니다.
yaml.load는 설정을 실행한다
완전한 YAML은 임의의 객체를 구성하는 언어별 태그를 지원합니다. 파이썬에서는 !!python/object/apply:os.system을 담은 문서가 파싱 도중에 명령을 실행할 수 있다는 뜻입니다. yaml.safe_load가 표준 타입만 만드는 버전이고, 직접 쓰지 않은 모든 것에는 이쪽을 써야 합니다. PyYAML은 결국 명시적인 loader 인자를 요구해 실수하기 어렵게 만들었지만, 그 이전의 코드는 수두룩하고, 다른 여러 언어에는 안전하지 않은 기본값이 함수 호출 하나 거리에 아직 남아 있습니다.
import yaml
with open("config.yaml") as f:
cfg = yaml.safe_load(f) # yaml.load 가 아니다
상위집합이라는 세부 사항
YAML 1.2는 JSON의 상위집합으로 설계되었고, 명세는 모든 유효한 JSON 문서가 유효한 YAML 1.2 문서이기도 하다고 말합니다. 그러니 1.2 파서는 여러분의 JSON을 읽습니다. YAML 1.1은 꼭 그렇지는 않습니다. 콜론 뒤에 공백을 요구하므로 붙여 쓴 {"a":1}은 거기서 파싱 오류이고, 1.1의 해석 규칙은 여전히 여러분의 문자열 일부를 불리언으로 바꿉니다. “그냥 JSON을 YAML 파서에 넣으면 된다”에 기대고 있다면, 먼저 라이브러리가 어느 판을 구현했는지 확인하세요. 어느 쪽이든 반대 방향으로는 깔끔하게 갈 수 있습니다. YAML → JSON 변환기를 쓰세요.
TOML: 중첩되기 전까지는 모호함이 없다
TOML이 존재하는 이유는 INI 파일이 쾌적하면서 부정확했기 때문입니다. TOML은 그 부정확함을 고쳤습니다. 정수와 실수는 별개의 타입이고, 불리언은 true와 false뿐이며, 날짜와 시간의 진짜 타입이 넷(오프셋 있는 날짜시간, 로컬 날짜시간, 로컬 날짜, 로컬 시간) 문법에 내장되어 있어 문자열에 숨겨 들일 필요가 없습니다. 주석은 일급입니다. 같은 키를 두 번 정의하는 것은 미정의 동작이 아니라 확실한 오류이고, 작은 일이지만 병합 실수의 실제 한 부류를 잡아냅니다.
평평하거나 얕은 설정이라면 셋 중 최고입니다. Cargo.toml과 pyproject.toml이 분명한 사례입니다. 섹션 몇 개, 문자열과 리스트 값, 가끔 한 단계 중첩. 모호한 것도 없고, 안전을 위해 따옴표를 칠 것도 없습니다.
데이터가 트리가 되면 금세 볼썽사나워집니다. 깊은 중첩은 긴 점 표기 헤더나 긴 점 표기 키 중 하나를 강요합니다.
[servers.production.database.replica]
host = "10.0.0.4"
port = 5432
그리고 객체의 배열에는 원소마다 반복하는 이중 대괄호 테이블 배열 형식이 필요합니다.
[[targets]]
name = "web"
port = 8080
[[targets]]
name = "worker"
port = 8081
항목이 둘이면 읽힙니다. 필드 세 개짜리 항목이 서른 개가 되고, 인라인 테이블은 한 줄에 들어가야 한다면, 여러분은 형식과 싸우고 있는 것입니다. 설정이 진짜로 계층적이라면 TOML은 맞지 않는 모양이고, 층을 하나 더할 때마다 그것을 느끼게 됩니다.
셋 다 주지 않는 것
십진 타입. 셋 다 주는 것은 실수, 즉 이진 근사입니다. 돈은 여전히 최소 단위의 정수나 문자열에 속합니다.
이진 데이터. JSON과 TOML에는 표현이 아예 없어서 문자열 속 base64가 됩니다. YAML에는 !!binary 태그가 있고, 작동은 하지만 다른 두 형식으로의 변환을 살아남지 못합니다.
형식에 딸려 오는 스키마. JSON 스키마가 성숙한 선택지이고, YAML 1.2가 같은 데이터 모델로 대응되므로 그것으로 YAML도 검증할 수 있습니다. 실제 YAML 검증의 대부분이 그렇게 이뤄집니다. TOML에는 비슷한 수준으로 자리 잡은 대응물이 없습니다.
변환을 통과하는 주석. 이것이 일방통행 문입니다. 주석은 문법에 살지 데이터 모델에 살지 않으므로, JSON으로 변환된 YAML이나 TOML 파일은 모든 주석을 영구히 잃고, 그것을 되찾아 주는 영리한 도구 같은 건 없습니다. 어떤 파일의 주석이 무게를 지탱한다면, 진실의 원본은 그 파일이고 JSON은 여러분이 생성하는 산출물일 뿐입니다. JSON에 주석이 없는 것은 의도적이며, 이 비대칭이 존재하는 이유이기도 합니다.
질문 형태의 선택
읽는 쪽이 기계뿐인가? JSON입니다. API가 YAML을 말하게 하지 마세요.
사람이 매주 고치고, 계층적인가? YAML입니다. 따옴표 규율과 safe_load를 곁들여서.
사람이 고치고, 대체로 평평한 섹션의 스칼라인가? TOML입니다. 잃는 것 없이 모호하지 않은 타입을 얻습니다.
주석이 살아남아야 하는가? 무엇을 고르든 그 파일이 진실의 원본입니다. 아래로 생성하고, 생성된 사본은 절대 고치지 마세요.
값을 만드는 쪽이 개발자가 아닌 사람이나 UI인가? JSON을, 프로그램이 생성하고 스키마로 검증해서 쓰세요. 위에 적은 YAML 함정은 전부 누군가 입력한 문자열이 방아쇠입니다.
지금 이 순간 서로 변환하는 중인가? JSON → YAML 변환기에서 하고, 출력을 믿지 말고 읽으세요. 특히 불리언을요. 그리고 배포와 닿는 무언가에 도달하기 전에 결과를 검증기에 통과시키세요.