YAML 기초·문법·anchor

RAGConfig·GitHub Actions·_quarto.yml·docker-compose가 모두 의존하는 형식

MINERVA의 RAGConfig YAML, A/B 실험 정의, GitHub Actions 워크플로, _quarto.yml, docker-compose 파일이 모두 YAML이다. 본 글은 들여쓰기·multiline string·anchor·alias·merge key 같은 핵심 문법과 자주 발생하는 함정을 정리한다. JSON과의 관계, 환경변수 보간 패턴까지.

Engineering
저자

Kwangmin Kim

공개

2026년 05월 06일

1 왜 YAML을 알아야 하는가

MINERVA 시리즈와 블로그 인프라 전반에 YAML이 분포한다.

용도 파일 의존 글
RAGConfig 프로파일 data/configs/default.yaml 11-0편
A/B 실험 정의 data/experiments/*.yaml 06편
Docker Compose docker-compose.yml 07-0편
GitHub Actions 워크플로 .github/workflows/*.yml 07-1편
Kubernetes Deployment k8s/*.yaml 11-1편
블로그 사이트 설정 _quarto.yml, _metadata.yml (블로그 자체)
Quarto 포스트 frontmatter 모든 .qmd 파일 상단 (블로그 자체)

같은 형식이지만 들여쓰기 함정·multiline string·anchor 같은 세부에서 자주 막힌다. 본 글이 그 토대를 한 호흡으로 정리한다.

2 핵심 데이터 타입

정의: YAML

YAML(YAML Ain’t Markup Language)은 사람이 읽기 쉬운 데이터 직렬화 형식. 핵심 데이터 모델:

  • scalar: 단일 값 (string, number, boolean, null)
  • sequence: 순서 있는 리스트
  • mapping: 키-값 쌍 (dict)
# scalar
name: minerva                        # string
version: 0.1.0                       # string (자동 추론)
port: 8000                           # int
temperature: 0.7                     # float
debug: true                          # boolean
description: null                    # null (또는 ~ 또는 빈 값)

# sequence (list)
agents:
  - qna_chatbot
  - data_standardizer
  - insilico_analyzer

# mapping (dict)
azure:
  endpoint: https://my-resource.openai.azure.com/
  api_version: "2024-12-01-preview"
  models:
    - gpt-4.1
    - gpt-5-mini

# inline 형식 (JSON 같음)
inline_list: [a, b, c]
inline_dict: {host: localhost, port: 8000}

scalar는 자동 타입 추론된다 — 8000은 int, "8000"은 string. 명확히 string으로 두려면 따옴표를 둘러싼다.

3 들여쓰기 — 공백만, 일관되게

# CORRECT — 공백 2칸
parent:
  child:
    grandchild: value

# WRONG — tab 사용 (YAML은 tab 금지)
parent:
    child:  value

YAML은 tab을 절대 사용 금지한다 (스펙 명시). 공백 2칸이 관행이고 4칸도 가능하나 한 파일 내에서 일관되어야 한다.

들여쓰기 1칸 차이가 의미를 바꾼다
# A
list_of_dicts:
  - name: alice
    age: 30
  - name: bob
    age: 25

# B
list_of_dicts:
  - name: alice
  age: 30                            # ← 들여쓰기 안 됨, list 항목이 아닌 root 키

A는 dict 2개의 list. B는 dict 1개 list + 별도 키 age. 들여쓰기 시각 검사가 필수.

대부분의 IDE(VS Code, IntelliJ)는 YAML 플러그인으로 들여쓰기 오류를 즉시 시각화한다. 사용을 강력 권장.

4 따옴표 — 언제 쓰는가

# 1. 자동 추론 — 따옴표 없어도 됨
name: minerva
port: 8000
debug: true

# 2. 의도한 string인데 자동 추론으로 다른 타입이 되는 경우
version: "0.1.0"                     # 따옴표 없으면 일부 파서가 0.1로 해석
phone: "010-1234-5678"               # 일부 파서가 산술식으로 해석 시도
boolean_string: "true"               # 따옴표 없으면 boolean True

# 3. 특수 문자 포함
description: "콜론: 포함"              # 따옴표 없으면 mapping으로 오인
template: "변수 ${VAR} 포함"

# 4. 작은따옴표 vs 큰따옴표
single: 'hello\nworld'               # \n이 그대로 두 글자
double: "hello\nworld"               # \n이 줄바꿈으로 해석

관행: scalar에 의심이 가면 따옴표 두기. 큰따옴표는 escape 가능, 작은따옴표는 literal.

5 multiline string — | > 차이

긴 텍스트를 여러 줄로 작성할 때 4가지 스타일이 있다.

# 1. literal block — | (줄바꿈 보존)
description: |
  Phase B에서 만든 LCEL Chain 파이프라인의 한계를 짚고,
  LangGraph StateGraph가 그 한계를 어떻게 푸는지 정리한다.
  Phase C-2 전환의 이론적 출발점이다.
# 결과: "Phase B에서 만든 LCEL Chain 파이프라인의 한계를 짚고,\nLangGraph...\nPhase..."
# 끝에 줄바꿈 1개 추가됨

# 2. folded block — > (줄바꿈을 공백으로 폴드)
description: >
  Phase B에서 만든 LCEL Chain 파이프라인의 한계를 짚고,
  LangGraph StateGraph가 그 한계를 어떻게 푸는지 정리한다.
# 결과: "Phase B에서 만든... 짚고, LangGraph... 정리한다.\n"
# 줄바꿈이 공백으로 변환됨

# 3. literal strip — |- (끝 줄바꿈 제거)
content: |-
  no trailing newline

# 4. folded strip — >- (끝 줄바꿈 제거 + 폴드)
brief: >-
  한 줄로 합쳐지고
  끝에 줄바꿈 없음
스타일 줄바꿈 끝 줄바꿈 사용처
\| 보존 1개 추가 코드 블록, 시·구조 있는 텍스트
> 공백으로 폴드 1개 추가 긴 설명문 (한 단락)
\|- 보존 제거 줄바꿈 의미 있되 끝 줄바꿈은 불필요
>- 폴드 제거 한 줄로 합치고 끝 깔끔하게

Quarto 포스트 frontmatter의 description: | 패턴이 literal block이다. MINERVA 시리즈 모든 글이 이 패턴.

---
title: "MINERVA 데이터 흐름 추적"
description: |
  MINERVA에서 사용자 질문이 최종 답변이 되기까지 거치는 모든 변환 단계를 추적한다.
  RunRequest 파싱 → 실험 라우팅 → ...
---

6 anchor & alias — & *

같은 값을 여러 곳에서 재사용하려면 anchor(&)로 정의하고 alias(*)로 참조한다.

# anchor 정의
defaults: &default_config
  timeout: 60
  max_retries: 3
  log_level: INFO

# alias 사용 — 위 dict를 그대로 가져옴
production_config:
  <<: *default_config              # merge key (아래 절 참조)
  timeout: 120                      # override

development_config:
  <<: *default_config
  log_level: DEBUG

GitHub Actions·docker-compose에서 자주 쓰는 패턴. 같은 환경변수를 여러 job에 반복 작성하지 않고 한 번 정의 후 alias.

# .github/workflows/example.yml
common-env: &common
  PYTHONPATH: ./src
  LOG_LEVEL: INFO

jobs:
  test:
    env:
      <<: *common
      DEBUG: true                   # 추가
  build:
    env:
      <<: *common                   # 같은 env 재사용
일부 도구는 anchor를 지원 안 한다

GitHub Actions는 anchor를 지원하지만, 다른 일부 CI(예: 일부 GitLab CI 버전)는 제한적. 사용 전 도구 문서 확인. 또한 yaml.safe_load(content)(Python PyYAML)는 anchor 지원 — 표준 YAML 1.1/1.2의 일부.

7 merge key — <<:

<<: *anchor는 anchor의 dict를 현재 dict에 병합한다.

base: &base
  a: 1
  b: 2

merged:
  <<: *base                         # a, b 가져옴
  c: 3                              # 추가
  b: 999                            # b override

# 결과: {a: 1, b: 999, c: 3}

여러 anchor를 동시에 병합:

defaults_a: &a
  x: 1
defaults_b: &b
  y: 2

combined:
  <<: [*a, *b]                      # 둘 다 병합
  z: 3
# 결과: {x: 1, y: 2, z: 3}

merge key는 YAML 1.1 스펙의 확장 기능. 대부분 라이브러리가 지원하지만 엄격 1.2 모드에서는 제외될 수 있다.

8 환경변수 보간 — 도구별 차이

YAML 자체는 환경변수 보간을 정의하지 않는다. 도구마다 다른 문법을 추가한다.

도구 문법 예시
docker-compose ${VAR} 또는 ${VAR:-default} image: ${IMAGE:-minerva:latest}
GitHub Actions ${{ env.VAR }} 또는 ${{ secrets.X }} key: ${{ secrets.AZURE_API_KEY }}
ansible { var } (Jinja2) host: {{ inventory_hostname }}
MINERVA RAGConfig ${VAR:default} (자체 구현) model: ${AZURE_DEPLOY:gpt-4.1}

MINERVA 11-0편${VAR:default}는 표준 docker-compose 문법(${VAR:-default})과 살짝 다른 자체 구현이다. 도구마다 정확한 문법 확인 필수.

9 JSON과의 관계

YAML 1.2부터 JSON은 YAML의 부분집합이다. 즉:

{"name": "minerva", "agents": ["qna", "ds"]}

이 JSON은 그대로 유효한 YAML이다. inline mapping/sequence가 JSON 문법과 동일하기 때문.

이 호환성 덕분에:

  • JSON 설정 파일을 YAML로 점진 마이그레이션 가능
  • YAML 파서가 JSON도 읽음
  • 일부 도구(GitHub Actions)는 yml 또는 json 둘 다 받음

차이점 — 둘 중 무엇을 쓸지:

기준 YAML JSON
사람이 작성 들여쓰기 + multiline string으로 가독성 우수 따옴표·콤마 부담
기계 파싱 속도 느림 (들여쓰기·anchor 처리) 빠름
주석 # 가능 불가
사람이 직접 편집하는 설정 권장 비권장
API 응답·로그 비권장 권장

운영 사용 — 사람이 작성·편집하는 설정은 YAML, 기계 간 데이터 교환은 JSON.

10 Python에서 YAML 읽기·쓰기

import yaml

# 읽기 — 항상 safe_load 사용
with open("config.yaml") as f:
    config = yaml.safe_load(f)
print(config["azure"]["endpoint"])

# 쓰기
data = {"name": "minerva", "agents": ["qna", "ds"]}
with open("output.yaml", "w") as f:
    yaml.safe_dump(data, f, default_flow_style=False, allow_unicode=True)
yaml.load 보안 위험

PyYAML의 yaml.load(content)는 임의 Python 객체를 역직렬화할 수 있다 — !!python/object 태그가 코드 실행으로 이어진다. 신뢰하지 않는 YAML은 반드시 yaml.safe_load(content)를 사용한다.

# 위험 — 신뢰하지 않는 입력
data = yaml.load(untrusted_yaml, Loader=yaml.Loader)  # 코드 실행 가능

# 안전
data = yaml.safe_load(untrusted_yaml)                  # 기본 타입만 허용

11 자주 발생하는 오류 패턴

WRONG:

parent:
    child: value                     # tab 사용 — YAML 파서 에러

CORRECT:

parent:
  child: value                       # 공백 2칸

YAML은 tab을 절대 허용하지 않는다. IDE의 “Insert Spaces” 설정을 켜고 .editorconfig로 강제. 들여쓰기 보이지 않는 차이는 가장 흔한 함정.

WRONG:

version: 1.0                         # 1.0이 float로 파싱됨
api_version: 2024-08-01              # date로 파싱될 수 있음

CORRECT:

version: "1.0"                       # 명시적 string
api_version: "2024-08-01"            # 명시적 string

버전 번호·날짜·전화번호 같은 string은 따옴표로 감싼다. 자동 타입 추론이 의도와 다르게 동작.

WRONG:

country: NO                          # YAML 1.1에서 false (Norway가 아님)
answer: yes                          # boolean true (string "yes" 아님)

CORRECT:

country: "NO"                        # 명시적 string
answer: "yes"

YAML 1.1은 yes/no/on/off도 boolean으로 추론한다. 1.2부터는 true/false만 boolean이지만 여전히 일부 파서가 1.1 호환 모드. 의심스러우면 따옴표.

WRONG:

description: |
  첫 줄
   두 번째 줄              # 추가 들여쓰기 — 의도와 다른 결과

CORRECT:

description: |
  첫 줄
  두 번째 줄              # 모든 줄 같은 들여쓰기

literal block(|) 안에서는 첫 줄의 들여쓰기 깊이가 기준이 되고, 그 이상 들여쓴 부분은 들여쓰기 자체가 본문에 포함된다. 의도가 아닌 한 모든 줄을 같은 깊이로.

WRONG:

import yaml

with open(user_uploaded_file) as f:
    data = yaml.load(f, Loader=yaml.Loader)   # !!python/object 태그로 코드 실행 가능

CORRECT:

data = yaml.safe_load(f)                       # 기본 타입만

신뢰하지 않는 YAML(사용자 업로드, 외부 API 응답 등)은 safe_load를 사용한다. CVE 패턴.

12 정리

영역 핵심
데이터 타입 scalar / sequence / mapping. 자동 타입 추론
들여쓰기 공백만 (tab 금지), 일관된 깊이
따옴표 의심 시 둘러싸기. 큰따옴표는 escape, 작은따옴표는 literal
multiline \| 줄바꿈 보존, > 폴드, - 끝 줄바꿈 제거
anchor·alias & *로 재사용, <<: merge key로 병합
환경변수 보간 도구마다 문법 다름 (${VAR}, ${{ secrets.X }}, { var })
JSON 호환 JSON은 YAML 1.2의 부분집합
Python 사용 yaml.safe_load 필수 (보안)

13 응용 분야

MINERVA 사용처 본 글 절
data/configs/*.yaml (RAGConfig) 기본 데이터 타입, multiline, 환경변수 보간
data/experiments/*.yaml (A/B arm) mapping + sequence, anchor로 공통 arm 추출
.github/workflows/*.yml merge key로 공통 env 재사용, 환경변수 보간
docker-compose.yml 환경변수 보간 + anchor
.qmd frontmatter multiline description: \|
_quarto.yml 깊은 mapping + sequence

14 관련 주제

선행 학습

바로 이어 읽을 글 (Tier 2 다음 편)

  • Python logging + structured log — 작성 예정
  • Docker Compose 기초 — 작성 예정

MINERVA 시리즈 응용

Subscribe

Enjoy this blog? Get notified of new posts by email: