YAML을 옹호하며

4 days ago 13
  • YAML 1.2는 사람이 직접 작성하는 중첩 구성 파일을 위한 들여쓰기 기반 직렬화 형식이며, 오래된 PyYAML 경험에서 비롯된 불안정성 비판과 구분 필요
  • 구성 파일 형식은 INI의 평면성, XML의 장황함, JSON의 주석·여러 줄 문자열 부재처럼 이전 세대의 한계를 바로잡는 흐름으로 변화해 왔음
  • TOML은 pyproject.toml과 Cargo.toml처럼 얕은 구조에서 명확하지만, 깊은 중첩 구조에서는 경로 반복과 배열 테이블 때문에 읽기·편집 부담 증가
  • YAML 1.2 Core Schema는 yes, no, on, off, y, n을 문자열로 처리하고 60진수 숫자와 핵심 타입으로서의 타임스탬프를 제거해 과거의 암시적 타입 추론 문제 축소
  • py-yaml12는 Python용 YAML 1.2 파서·포매터로, Rust 구현을 통해 안전한 기본값, yaml-test-suite 100% 준수, PyYAML C 확장과 경쟁 가능한 성능 제공

문제의식

  • 최근 몇 년 사이 구성 파일 형식에 대한 분위기는 YAML은 나쁘고 TOML은 좋다는 쪽으로 이동했지만, YAML 평가는 역사, 명세 변화, 2026년 도구 상태를 함께 봐야 하는 문제
  • YAML 비판은 오랫동안 합리적이었고 신중한 사용자까지 예상 밖 동작을 겪었지만, 명세는 변화했고 도구 생태계도 따라잡는 중
  • 현재의 YAML 비판은 구성 파일 형식의 계보를 함께 봐야 이해 가능하며, 이전 형식의 과잉을 다음 형식이 바로잡는 논쟁이 반복되어 온 흐름

구성 파일 형식의 간략한 역사

  • INI 파일은 1980년대 초 MS-DOS와 초기 Windows와 함께 등장했고, 키-값 쌍, 대괄호 섹션, 세미콜론 주석을 갖춘 평면적이고 읽기 쉬운 사람이 편집 가능한 형식
  • INI는 장치 드라이버 구성, 글꼴 경로 지정, 애플리케이션 설정 같은 당시 요구에는 충분했지만, 한 단계 이상 중첩할 수 없고 공식 명세가 없어 파서마다 방언을 구현하는 구조적 한계
  • XML은 1990년대 후반 엔터프라이즈 소프트웨어 세계에서 널리 채택됐고, 임의 계층, 스키마, 네임스페이스, 변환, 자기 설명 구조를 제공했지만 실제 구성 파일은 매우 장황해져 손으로 유지보수하기 어려운 형식
  • JSON은 XML에 대한 가벼운 반응으로 등장했고 JavaScript 객체 리터럴의 단순성을 바탕으로 2000년대 후반과 2010년대 초반 웹 API에서 XML을 대체했지만, 구성 파일로 쓰기에는 주석 부재, 여러 줄 문자열 부재, 후행 쉼표 금지라는 제약
  • YAML은 2001년, TOML은 2013년에 JSON이 남긴 틈을 메우기 위해 등장했으며, YAML은 들여쓰기 기반 문법으로 임의 중첩, 여러 문서, 참조, 사용자 정의 타입을 제공하고 TOML은 명시적 타입과 공식 명세를 갖춘 “표준화된 INI”를 지향
  • 각 세대의 새 형식은 이전 형식의 과잉을 출발점으로 삼았고, 현재 YAML 문제의 상당 부분은 형식 설계 자체보다 특정 명세 버전과 그 버전에 묶인 파서의 산물

YAML 반대 근거였던 문제들

  • YAML 비판은 조작된 문제가 아니라 여러 해 동안 실제 프로그래머가 겪은 경험에 기반한 문제
  • 가장 유명한 Norway 문제는 YAML 1.1에서 따옴표 없는 스칼라 NO가 불리언 false로 해석돼 국가 코드 목록의 no가 거짓값으로 바뀌는 동작 countries: - dk - fi - is - no - se ["dk", "fi", "is", false, "se"]
  • 같은 규칙은 yes, on, off, y, n과 여러 대소문자 변형에도 적용됐고, 22:22 같은 포트 매핑은 60진수 정수로, 10.23 같은 버전 번호는 문자열이 아니라 부동소수점으로, 날짜처럼 보이는 값은 타임스탬프로 해석되는 문제
  • 데이터 과학과 머신러닝 코드에서 자연스러운 변수명인 n과 y도 YAML 1.1의 암시적 불리언 규칙 아래에서는 문자열 키가 아니라 False, True 키로 해석될 수 있는 구조 {"variables": {"x": "features", False: "sample_size", True: "target"}}
  • YAML 1.1의 공격적인 암시적 타입 추론은 따옴표 없는 true를 불리언으로 읽는 가독성을 의도했지만, 실제 구성 파일에서는 예측 불가능성이 커지는 결과
  • 구성 파일은 자주 편집되지 않고 원래 작성자가 아닌 사람이 수정하는 경우가 많으며, 조용한 오파싱이 수개월 동안 시스템 전반에 전파될 수 있어 예상 밖 동작을 가장 견디기 어려운 영역
  • 전체 YAML 명세의 복잡성도 부담으로 작용했고, YAML 1.2.2 명세는 10개 장과 네 단계 번호가 붙은 섹션 구조를 가진 상당한 문서
  • 여러 줄 문자열 표현 방식이 많고, 앵커와 별칭은 강력한 참조 시스템을 만들지만 대부분의 구성 작업에는 필요한 수준을 넘어서는 개념적 무게
  • 태그 기반 객체 역직렬화의 보안 문제, 특히 Python의 yaml.load() 취약점은 잘 알려진 공격 벡터가 되었고, 이러한 비판은 YAML 1.1과 그 주변 도구 생태계에 대해 유효했던 지점

TOML이 잘하는 점

  • TOML은 평면적이거나 얕은 구성 구조에서 깔끔하고 읽기 쉬우며 모호하지 않은 형식
  • TOML 문법은 INI 파일을 본 사람에게 익숙하면서도 명시적 타입, 공식 명세, 점으로 구분한 키를 통한 중첩 테이블 지원을 추가한 구조
  • pyproject.toml과 Cargo.toml은 보통 한두 단계 깊이의 잘 정의된 섹션과 예측 가능한 내용을 갖기 때문에 TOML이 잘 맞는 사례
  • TOML에서는 문자열이 항상 따옴표로 감싸져 no가 불리언인지 단어인지 모호하지 않고, 정수·부동소수점·날짜가 1급 타입이며, 주석도 기대한 방식으로 동작
  • Python 패키징 생태계의 PEP 518 채택과 Rust 커뮤니티의 Cargo 사용은 이런 문제 범위에서 TOML이 적합한 선택이라는 근거
  • TOML 명세는 유능한 프로그래머가 주말에 호환 파서를 작성할 수 있을 정도로 짧고, 그 결과 파서 생태계가 크고 잘 테스트되며 일관적인 장점
  • TOML에는 YAML 1.1과 1.2 사이의 버전 분열에 해당하는 문제가 없고, TOML 1.0은 어디서나 TOML 1.0이라는 일관성

TOML이 버거워지는 지점

  • 구성 파일이 깊이를 표현해야 할 때 TOML의 중첩 구조는 점으로 구분한 섹션 헤더([servers.alpha])나 테이블 배열([[products]])에 의존하며, 중첩이 늘어날수록 읽기 어려워지는 구조
  • PyTOML의 Martin Vejnár는 자신의 라이브러리가 pip 의존성이 되는 것을 거절하면서, 구성 스키마가 복잡해지자 TOML 문법이 보기 나쁘고 읽기 어렵다는 경험을 근거로 제시
  • YAML에서는 들여쓰기가 계층을 한눈에 전달하지만, TOML에서는 각 섹션 헤더에 전체 경로를 반복해야 하는 차이 services: web: image: nginx:latest environment: DB_HOST: postgres DB_PORT: 5432 resources: limits: memory: 512M cpu: "0.5" [services.web] image = "nginx:latest" [services.web.environment] DB_HOST = "postgres" DB_PORT = 5432 [services.web.resources.limits] memory = "512M" cpu = "0.5"
  • TOML 독자는 평평한 한정 이름의 연속에서 트리 구조를 머릿속으로 재구성해야 하며, StrictYAML 문서의 측정에서는 같은 데이터를 표현할 때 TOML 파일이 반복 경로 접두사 때문에 약 50% 더 많은 문자를 사용
  • Python은 들여쓰기를 구조로 쓰는 방식이 약점이 아니라 강점임을 오래전에 보여줬고, 시각적 구조와 문법 구조가 어긋나는 버그 종류를 제거하는 효과
  • YAML은 이런 들여쓰기 구조의 장점을 물려받지만, TOML은 들여쓰기를 요구하지 않아 키와 포함 테이블의 관계가 파일의 물리적 배치가 아니라 섹션 헤더에만 존재
  • 깊게 중첩된 구성 파일에서 TOML은 스캔하기 어렵고 확신을 갖고 편집하기 어려운 형식

YAML 1.2의 변경점

  • YAML 1.2 명세는 2009년에 발표됐고, 명확화 개정판인 1.2.2는 2021년 10월 완료
  • YAML 1.2 Core Schema에서는 true, false와 True, False, TRUE, FALSE만 불리언으로 인식하며, yes, no, on, off, y, n은 일반 문자열
  • 22:22 문제를 만든 60진수 숫자 리터럴은 완전히 제거됐고, 타임스탬프는 더 이상 핵심 타입이 아니므로 Core Schema에서 따옴표 없는 2026-05-05는 자동 감지 날짜가 아니라 문자열
  • JSON은 YAML 1.2의 엄격하고 올바른 부분집합이 되었으며, 유효한 JSON 문서는 YAML로도 동일하게 파싱
  • 태그 해석 규칙은 더 엄격하고 명확해졌고, 명세 자체도 더 분명하게 작성되며 GitHub에서 공개적으로 유지관리
  • 사람들이 비판해 온 YAML은 YAML 1.1이고, 오늘날 언어를 지배하는 명세는 더 안전하고 예측 가능한 YAML 1.2
  • 문제는 사용자의 YAML 경험이 명세가 아니라 파서를 통해 매개된다는 점이며, Python 사용자 대부분에게 그 파서는 YAML 1.1을 구현하고 2006년 이후 핵심 의미론을 바꾸지 않은 PyYAML

Python YAML 파서 지형

  • PyYAML은 Python의 사실상 표준 YAML 라이브러리이며, 성능을 위해 C 라이브러리 LibYAML을 감싸고 순수 Python 대체 경로도 제공
  • PyYAML은 매주 수백만 회 다운로드되고 수많은 패키지의 의존성이지만 YAML 1.1을 구현하며, Python 생태계에서 “YAML이 국가 코드를 불리언으로 파싱했다”는 경험의 뿌리
  • PyYAML 저장소에는 200개가 넘는 열린 이슈와 100개가 넘는 열린 풀 리퀘스트가 있고, 프로젝트는 유지관리 중이지만 느리게 움직이며 YAML 1.2 의미론으로의 주요 버전 전환은 실현되지 않은 상태
  • ruamel.yaml은 YAML 1.2 지원과 함께 주석, flow style, 키 순서를 보존하는 왕복 편집 기능을 제공하며, 주석 보존이나 형식 인식 편집에는 PyYAML보다 훨씬 강력한 선택지
  • ruamel.yaml은 기본 왕복 모드에서 주로 순수 Python 구현이기 때문에 PyYAML의 C 기반 빠른 경로보다 상당히 느리고, 네임스페이스 패키지 문제와 의존성 체인 때문에 배포 파이프라인을 혼란스럽게 한 패키징 이력
  • StrictYAML은 YAML의 의도적인 부분집합을 구현하며, 모든 암시적 타입 추론, 태그, 앵커, flow style을 제거한 형식
  • StrictYAML은 철학적으로 전체 YAML보다 TOML에 더 가깝고, YAML의 들여쓰기 문법을 쓰는 안전하고 단순한 형식이지만 Python 전용이며 다른 언어 구현도 없고 명세 준수도 목표가 아님
  • 이 지형에서 빠르고, 완전한 YAML 1.2 준수를 제공하며, PyYAML의 기본 인터페이스를 대체하기 쉬운 라이브러리가 부족했던 상황

py-yaml12 소개

  • py-yaml12는 Python용 YAML 1.2 파서·포매터이며, 속도와 정확성을 위해 Rust로 구현한 라이브러리
  • py-yaml12는 Rust YAML 라이브러리인 saphyr 크레이트 위에 구축됐고, 로딩용 parse_yaml(), read_yaml(), 직렬화용 format_yaml(), write_yaml()라는 작고 집중된 API 제공
  • 단순성

    • 대부분의 사용 사례에서 dict, list, int, float, str, None 같은 기본 Python 내장 타입만으로 처음부터 끝까지 작업하는 설계
    • 일반 경로에는 특별한 문서 클래스나 사용자 정의 노드 타입이 없고, YAML 1.2가 JSON의 상위집합이므로 모든 유효한 JSON은 동일하게 파싱
    • py-yaml12는 커뮤니티가 유지관리하는 엣지 케이스와 적합성 테스트 모음인 yaml-test-suite에서 100% 준수 달성
    • regions 목록의 no는 PyYAML의 YAML 1.1 동작에서는 조용히 False가 되지만, py-yaml12의 YAML 1.2 동작에서는 명세가 요구하는 문자열 "no"로 유지
    • 파일 API도 직접적이며, Python 딕셔너리를 디스크에 쓰고 다시 읽으면 같은 객체가 나오는 무손실 왕복 동작
    • 태그가 붙은 값 같은 고급 YAML 기능에는 Yaml 래퍼 타입을 제공하지만, 일반적인 구성 작업에서는 선택 사항
  • 안전성

    • py-yaml12의 기본값은 사용 편의성뿐 아니라 안전성도 높이며, PyYAML의 반대 접근은 태그를 명령처럼 취급해 YAML 파일을 읽는 것만으로 임의 Python 코드를 실행할 수 있는 위험
    • PyYAML의 Python object-apply 태그 네임스페이스를 별칭으로 만든 YAML 파일을 yaml.load(f, Loader=yaml.Loader)로 읽으면 평범한 딕셔너리를 반환하기 전에 Python 코드가 먼저 실행되는 구조
    • 예시에서는 파싱 결과가 {'debug': False, 'retries': 3}로 보이지만, 그 전에 환경 변수 YAML_PAYLOAD_RAN이 '1'로 설정되어 결과만 봐서는 실행 사실을 알 수 없는 문제
    • py-yaml12는 명시적으로 선택하지 않은 태그를 실행하지 않고 데이터로 유지하며, 처리되지 않은 태그는 값과 태그를 담은 Yaml 래퍼로 반환
  • 속도

    • py-yaml12 벤치마크는 킬로바이트부터 메가바이트까지의 파일 크기에서 읽기와 쓰기 성능을 PyYAML의 기본 순수 Python 경로, LibYAML 기반 CSafeLoader·CSafeDumper, ruamel.yaml과 비교
    • 핵심 파싱·포매팅 로직을 해석형 Python이 아니라 컴파일된 Rust로 구현했기 때문에, py-yaml12는 완전한 YAML 1.2 준수를 유지하면서 PyYAML의 C 확장과 경쟁 가능한 성능
    • 현재 Python 라이브러리 중 완전한 YAML 1.2 준수와 PyYAML C 확장급 성능을 함께 제공하는 선택지는 많지 않은 상황

결론

  • 일반적인 YAML 대 TOML 논쟁은 문제가 있던 형태로는 더 이상 존재하지 않는 형식을 상대로 한 논쟁에 가까우며, 비판은 실제였지만 역사적인 성격
  • YAML 비판은 명세와 제대로 구현된 YAML 1.2가 아니라 PyYAML을 통해 경험한 YAML 1.1을 가리키는 경우
  • TOML은 얕고 평평한 구성에는 여전히 좋은 선택이고 pyproject.toml도 그 역할에 잘 맞지만, YAML이 본질적으로 안전하지 않거나 예측 불가능하다는 주장은 호환 YAML 1.2 파서 앞에서는 성립하지 않음
  • 중요한 질문은 추상적으로 어느 형식이 최고인지가 아니라, 어떤 형식과 어떤 도구가 특정 작업에 잘 맞는지의 문제
  • 복잡하고 중첩된 사람이 작성하는 구성 파일에는 최신 파서를 갖춘 YAML 1.2가 강한 답이며, 형식은 이전 세대의 거친 부분을 다음 세대가 바로잡는 방식으로 개선
  • Python에서는 pip install py-yaml12로 현대적이고 명세를 준수하는 YAML 경험을 확인 가능
  • R 환경에서는 r-yaml12가 같은 이점을 제공하며, 완전한 YAML 1.2 준수, Rust 기반 성능, 안전한 기본값을 Python 패키지와 동일하게 제공
Read Entire Article