yaml에 대한 피로감의 핵심은 문법이 아니라 중복과 유지보수성 문제다. 포맷을 바꾸는 대신 루프와 수식 같은 추상화로 설정을 생성하자는 주장과, 그에 따른 절충점과 사용할 수 있는 도구들을 논한다.
URL: https://ruudvanasseldonk.com/2025/abstraction-not-syntax
Title: 구문이 아니라 추상화
공개일: 2025년 10월 12일
세상은 yaml에 점점 피로감을 느끼고 있다. 대체 구성 포맷들이 여기저기서 떠오르는 중이다. toml은 Cargo 같은 도구들과 Python 표준 라이브러리 채택의 영향도 있어 꾸준히 입지를 넓혀 왔다. 주석, 쉼표, 그리고 숫자 5까지 허용하는 json 상위집합들이 번성하고 있고, KDL, kson, 그리고 이제 maml까지 친근함과 단순함 사이의 달콤한 지점을 약속한다.
yaml이 해롭다고 나는 믿지만, 더 단순한 포맷들은 기본적으로 괜찮고, 그 차이는 대부분 피상적이다. 진짜 차이는 데이터 모델에 있다. 대부분의 포맷은 객체와 배열로 이루어진 json 데이터 모델을 채택하고, KDL, HCL, 그리고 예컨대 Nginx는 속성과 자식을 가진 이름 붙은 노드라는 XML 데이터 모델을 채택한다. 나머지는 전부 구문일 뿐이다. 그리고 그렇다, 구문도 중요하다. 하지만 난삽한 문법(라인 노이즈)이 여기서의 진짜 문제는 아니다!
예를 하나 보자. 백업을 저장할 클라우드 스토리지 버킷을 정의해야 한다고 하자. Alpha와 Bravo 두 데이터베이스를 백업하려고 한다. 각 데이터베이스마다 시간별, 일별, 월별 백업을 위한 버킷이 하나씩 필요하다. 각각의 버킷에는 4일, 30일, 365일 뒤에 백업을 삭제하는 수명 주기 정책이 있어야 한다. 클릭질은 하고 싶지 않으니, 다음과 같은 가상의 구성 파일을 사용하는 인프라스트럭처-애즈-코드 도구로 이걸 설정해 보자:
{
"buckets": [
{
"name": "alpha-hourly",
"region": "eu-west",
"lifecycle_policy": { "delete_after_seconds": 345600 }
},
{
"name": "alpha-daily",
"region": "eu-west",
"lifecycle_policy": { "delete_after_seconds": 2592000 }
},
{
"name": "alpha-monthly",
"region": "eu-west",
"lifecycle_policy": { "delete_after_seconds": 31536000 }
},
{
"name": "bravo-hourly",
"region": "us-west",
"lifecycle_policy": { "delete_after_seconds": 345600 }
},
{
"name": "bravo-daily",
"region": "eu-west",
"lifecycle_policy": { "delete_after_seconds": 259200 }
},
{
"name": "bravo-monthly",
"region": "eu-west",
"lifecycle_policy": { "delete_after_seconds": 31536000 }
}
]
}
그래, 이 파일은 다른 포맷으로 쓰면 더 친근해 보일 것이다. 하지만 파일에는 버그가 두 개 들어 있고, 포맷을 바꾼다고 그게 고쳐지지는 않는다. 알아맞히겠는가? 스포일러를 피하려고, 페이지를 채울 약간의 yaml을 더 넣겠다. 이해를 돕기 위해 주석도 덧붙였다:
buckets:
- name: "alpha-hourly"
region: "eu-west"
lifecycle_policy:
delete_after_seconds: 345600 # 4일
- name: "alpha-daily"
region: "eu-west"
lifecycle_policy:
delete_after_seconds: 2592000 # 30일
- name: "alpha-monthly"
region: "eu-west"
lifecycle_policy:
delete_after_seconds: 31536000 # 365일
- name: "bravo-hourly"
region: "us-west"
lifecycle_policy:
delete_after_seconds: 345600 # 4일
- name: "bravo-daily"
region: "eu-west"
lifecycle_policy:
delete_after_seconds: 259200 # 30일
- name: "bravo-monthly"
region: "eu-west"
lifecycle_policy:
delete_after_seconds: 31536000 # 365일
무엇이 잘못됐나?
bravo-hourly만 미국(us-west)에 있고, 다른 버킷들은 유럽(eu-west)에 있다.bravo-daily는 만료 시간 숫자에 0이 하나 빠져서, 의도한 30일이 아니라 3일만 백업을 보관한다.코드 리뷰에서 이걸 잡았을까? 이제 세 번째 데이터베이스 Charlie를 추가해야 한다고 해 보자. 세 단락을 복붙하고 bravo를 charlie로 바꾼다. 축하한다, 이제 버그까지 복제했다. 그리고 요즘은 LLM도 점점 이런 걸 따라 하기 시작했으니, 버그를 직접 복사할 필요조차 없어졌다!
다른 포맷을 채택하면 눈은 더 편해질 수 있어도, 중복은 줄지 않는다. 그러니 진짜 문제를 해결하지 못한다.
잡음을 줄이는 것은 고귀한 목표지만, 추상화를 가능하게 하는 것이 훨씬 더 영향력이 크다. 따옴표 스타일이나 끝의 쉼표 같은 걸로 바이크셰딩할 수 있지만, 실제로 필요한 건 _for 루프_다. 같은 구성을 RCL로 쓰면 이렇게 된다:
{
buckets = [
let period_retention_days = {
hourly = 4,
daily = 30,
monthly = 365,
};
for database in ["alpha", "bravo"]:
for period, days in period_retention_days:
{
name = f"{database}-{period}",
region = "eu-west",
lifecycle_policy = { delete_after_seconds = days * 24 * 3600 },
}
],
}
처음엔 조금 낯설 수 있지만, Python이나 Rust, TypeScript를 본 적이 있다면 이 파일을 읽을 수 있을 것이다. (좀 더 부드러운 소개가 필요하다면 튜토리얼을 보라.) 다음과 같이 두 부류의 버그를 없애고, 파일을 더 유지보수하기 쉽게 만들었다는 점에 주목하자:
charlie를 추가해야 한다면, 변경은 딱 한 줄이다.바로 여기가 진짜 이득이다!
생성형 구성을 쓰면 문제를 해결하는 동시에 새로운 문제도 만든다:
rcl build 같은 기능이 마찰을 줄여 주긴 하지만, 그래도 단계가 하나 더 늘어난다.구성 파일을 소스 관리 하에 두고 있다면, 생성된 파일까지 함께 체크인하여 이런 점들을 어느 정도 완화할 수 있다. (이단이라는 거, 안다!) 이렇게 하면 검색 가능성이 돌아오고, 생성기 코드에서의 변경도 볼 수 있을 뿐 아니라 생성된 파일에 어떤 영향이 생기는지도 함께 리뷰할 수 있다.
Cue, Dhall, Jsonnet, RCL 같은 구성 언어들은 반복적인 구성에서 보일러플레이트를 제거하도록 특별히 설계되었다. 하지만 굳이 새로운 도구를 들여올 필요는 없다. json이나 toml을 출력하는 약간의 Python이나 Nix만으로도 충분히 멀리 갈 수 있다. yaml은 json의 상위집합이므로, yaml을 받는 것은 무엇이든 json도 받는다는 점을 기억하자! 중복 제거는 복붙을 이기고, 데이터 구조를 조작하는 것은 문자열 템플릿보다 안전하다.
yaml에 대한 피로감이 커지고, 대체 구성 포맷들이 여기저기서 떠오르고 있다. yaml을 toml 같은 더 단순한 포맷으로 바꾸는 데 나는 박수를 보낸다. 보기 좋은 코드가 못생긴 코드보다 일하기 좋다는 것도 분명하다. 하지만 어떤 멀티라인 문자열 구문이 더 우월한가를 두고 다투는 건 더 깊은 문제를 놓치는 일이라고도 생각한다.
구성이 더 복잡해질수록, 우리가 정말로 필요한 것은 중복을 없애는 _추상화_다. 문자열 템플릿이 아니라, 데이터 구조 위의 진짜 추상화. 이는 구성 자체를, 적어도 어느 정도는, 코드로 바꾼다는 뜻이다. 코드와 데이터 사이의 균형을 어떻게 잡을지는 여전히 좋은 판단력의 문제지만, 충분히 큰 구성이라면 그 균형점이 100% 데이터 쪽에 있을 가능성은 낮다.