파일 한 곳에서 YAML 블록을 복사하여 다른 곳에 붙여넣은 적이 있다면, 앵커와 별칭이 해결하는 문제를 이미 겪은 겁니다. 대형 Kubernetes 설정, 다중 환경 Docker Compose 파일, 복잡한 CI 파이프라인은 같은 블록들 — 리소스 제한, 환경 변수, 볼륨 마운트 — 을 계속해서 반복하는 경향이 있습니다. YAML에는 이를 위한 내장 해답이 있으며, 대부분의 개발자는 그것이 존재한다는 것조차 모릅니다.

앵커(&)와 별칭(*)은 값을 한 번 정의하고 같은 파일 내 어디서든 참조할 수 있는 YAML의 메커니즘입니다. 앵커를 변수 선언, 별칭을 그 사용으로 생각하세요. 병합 키(<<)는 이를 객체 병합으로 확장합니다 — JavaScript의 스프레드 연산자와 유사합니다. 두 기능 모두 노드 앵커에 관한 YAML 1.2 사양 섹션에 정의되어 있습니다.

기본 앵커와 별칭 문법

문법은 최소화되어 있습니다. 값에 &name 접두사를 붙여 앵커를 만들고, 문서 어디서든 *name으로 참조하세요:

yaml
# Define the anchor once
default_timeout: &timeout 30

# Reference it multiple times
services:
  api:
    timeout: *timeout      # → 30
  worker:
    timeout: *timeout      # → 30
  scheduler:
    timeout: *timeout      # → 30

YAML 파서가 이것을 읽을 때, 모든 별칭은 앵커와 정확히 같은 값으로 해석됩니다. 앵커를 변경하면 모든 별칭이 업데이트됩니다. 검색-교체가 필요 없습니다.

<< 키를 사용한 객체 병합

스칼라 재사용도 유용하지만, 진짜 강점은 전체 매핑 블록을 병합하는 데서 옵니다. << 병합 키는 참조된 앵커의 모든 키-값 쌍을 현재 매핑에 흡수합니다:

yaml
# Define a base configuration block
base_service: &base
  restart: unless-stopped
  logging:
    driver: json-file
    options:
      max-size: "10m"
      max-file: "3"
  networks:
    - app-network

# Merge it into specific services
services:
  api:
    <<: *base              # merge all keys from base_service
    image: my-api:latest
    ports:
      - "8080:8080"

  worker:
    <<: *base              # same base config
    image: my-worker:latest
    command: ["node", "worker.js"]

  scheduler:
    <<: *base              # and again
    image: my-scheduler:latest
    command: ["node", "cron.js"]

매핑에 직접 정의된 키는 병합된 앵커의 키를 재정의합니다. 따라서 baserestart: unless-stopped를 설정하고 서비스도 restart: no를 설정하면, 서비스 수준의 값이 우선합니다.

이전과 이후: 실제 Docker Compose

앵커 없이 일반적인 다중 서비스 Docker Compose 파일이 어떻게 생겼는지 보여줍니다 — 반복으로 가득 차 있습니다:

yaml
# BEFORE — repetitive and hard to maintain
version: "3.9"
services:
  api:
    restart: unless-stopped
    env_file: .env
    networks: [app-network]
    logging:
      driver: json-file
      options:
        max-size: "10m"
    depends_on:
      - postgres

  worker:
    restart: unless-stopped
    env_file: .env
    networks: [app-network]
    logging:
      driver: json-file
      options:
        max-size: "10m"
    depends_on:
      - postgres

  mailer:
    restart: unless-stopped
    env_file: .env
    networks: [app-network]
    logging:
      driver: json-file
      options:
        max-size: "10m"
    depends_on:
      - postgres

이제 앵커와 병합 키를 사용하면 — 반복되는 모든 블록이 정확히 한 번 정의됩니다:

yaml
# AFTER — DRY and easy to maintain
version: "3.9"

x-common: &common
  restart: unless-stopped
  env_file: .env
  networks: [app-network]
  logging:
    driver: json-file
    options:
      max-size: "10m"
  depends_on:
    - postgres

services:
  api:
    <<: *common
    image: my-api:2.4.1
    ports:
      - "8080:8080"

  worker:
    <<: *common
    image: my-worker:2.4.1
    command: ["node", "worker.js"]

  mailer:
    <<: *common
    image: my-mailer:2.4.1
    command: ["node", "mailer.js"]
팁: x- 접두사는 확장 필드 (x-common 같은)에 대한 Docker Compose 관례로, Compose가 해당 블록을 서비스 정의로 파싱하려는 것을 방지합니다. Compose 엔진에서는 무시되지만 YAML 앵커로 완전히 사용할 수 있습니다.

GitHub Actions: 앵커를 사용한 매트릭스 전략

CI 설정은 앵커가 효과를 발휘하는 또 다른 곳입니다. 여러 작업에 걸쳐 공통 단계 설정을 재사용하는 GitHub Actions 워크플로입니다:

yaml
name: Build and Test

on: [push, pull_request]

# Shared setup steps
x-setup-steps: &setup-steps
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: "20"
      cache: npm
  - run: npm ci

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - *setup-steps
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - *setup-steps
      - run: npm test -- --coverage

  build:
    runs-on: ubuntu-latest
    steps:
      - *setup-steps
      - run: npm run build

참고: GitHub Actions의 YAML 파서는 앵커와 별칭을 지원합니다. 하지만 일부 CI 시스템 (특히 버전 2.1 이전의 CircleCI config.yml)은 자체적인 앵커 유사 확장 문법을 가지고 있습니다. 항상 특정 도구의 문서를 확인하세요.

Kubernetes: 리소스 제한 재사용

Kubernetes 매니페스트에서 리소스 제한과 환경 변수는 앵커의 최적 후보입니다. 제가 정기적으로 사용하는 패턴입니다:

yaml
# Anchors at the top of the file (or in a shared ConfigMap generator)
x-resources:
  small: &resources-small
    requests:
      memory: "64Mi"
      cpu: "50m"
    limits:
      memory: "256Mi"
      cpu: "200m"
  large: &resources-large
    requests:
      memory: "256Mi"
      cpu: "200m"
    limits:
      memory: "1Gi"
      cpu: "1000m"

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
spec:
  template:
    spec:
      containers:
        - name: frontend
          image: frontend:latest
          resources: *resources-small    # reuse small profile

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    spec:
      containers:
        - name: api
          image: api:latest
          resources: *resources-large    # reuse large profile

다중 병합과 재정의 우선순위

단일 매핑에서 여러 앵커를 병합할 수 있습니다. << 아래에 목록으로 제공하세요. 중복 키의 경우 목록의 앞쪽 항목이 뒷쪽 항목보다 우선합니다:

yaml
defaults: &defaults
  color: blue
  size: medium
  weight: light

overrides: &overrides
  color: red          # will override defaults.color
  weight: heavy       # will override defaults.weight

result:
  <<: [*overrides, *defaults]    # overrides takes priority
  name: widget
# Result: color=red, size=medium, weight=heavy, name=widget

알아야 할 제한사항

  • 같은 파일 내에서만. 앵커와 별칭은 단일 YAML 문서 스트림 내에서만 작동합니다. 다른 파일의 앵커를 참조할 수 없습니다. 파일 간 재사용에는 도구 고유의 메커니즘이 필요합니다 (예: Helm 템플릿, Kustomize, 또는 ytt 같은 YAML 프로세서).
  • 계산된 값 없음. 별칭은 앵커 값의 정확한 복사본으로 해석됩니다. 보간이나 템플릿 로직이 없습니다 — 그것은 Helm이나 Jinja2 같은 도구를 위한 것입니다.
  • 병합 키는 YAML 확장입니다. << 병합 키는 코어 사양이 아닌 YAML 1.1 확장에 정의되어 있습니다. 대부분의 파서가 지원하지만, 엄격하게 사양을 준수하는 YAML 1.2 파서는 지원하지 않을 수 있습니다.
  • 순환 참조는 파서를 중단시킵니다. 노드에 앵커를 달고 그 안에서 참조하려 하지 마세요. 기술적으로 유효하지 않습니다.
  • 별칭은 복사이지 참조가 아닙니다. 코드에서 해석된 별칭 값을 수정해도 앵커나 다른 별칭은 변경되지 않습니다. 파싱 후에는 독립적인 값입니다.

마무리

YAML 앵커와 별칭은 형식에서 가장 덜 사용되는 기능 중 하나입니다. 같은 설정 블록이 두 번 이상 나타나는 약 50줄 이상의 YAML 파일에서는 앵커가 거의 분명히 있어야 합니다. 유지 보수성의 이전/이후 차이는 극적입니다 — 파일 전체를 뒤지는 대신 리소스 제한 프로필 하나나 공유 환경 변수 하나만 변경하면 됩니다. 앵커가 있는 파일을 정돈되게 유지하려면 YAML 포매터를 사용하고, 앵커 참조가 올바르게 해석되는지 확인하려면 YAML 유효성 검사기를 사용하세요.