LLM WikiAccess-protected knowledge portal
← 스터디 홈
4편 · 약 24분

스키마 변경 안전 배포: 하위 호환, 마이그레이션, Contract 검증

스키마 변경이 사고를 만드는 방식

데이터 파이프라인 장애의 상당수는 스키마 변경에서 시작된다. 컬럼을 삭제했더니 하위 DAG이 KeyError를 던지거나, 컬럼 타입을 바꿨더니 Spark 잡이 직렬화 오류로 죽는다. 애플리케이션 DB라면 서버 재시작으로 한 버전에 고정할 수 있지만, 데이터 플랫폼은 다르다. Kafka 토픽을 읽는 컨슈머, Spark 잡, dbt 모델, BI 쿼리가 서로 다른 시점에 서로 다른 스키마를 기대하며 동시에 동작한다.

안전한 스키마 변경이란 "구버전과 신버전 코드가 동시에 실행되는 기간" 동안 두 버전 모두 정상 동작하도록 스키마를 설계하는 것이다. 이를 위한 핵심 개념이 하위 호환성(backward compatibility)과 Expand-Contract 패턴이다.


하위 호환성 분류

호환성에는 두 방향이 있다.

방향의미실무 예시
Backward compatible새 스키마로 쓴 데이터를 구버전 리더가 읽을 수 있음새 컬럼 추가 후 기존 컨슈머가 무시하고 처리
Forward compatible구버전 스키마로 쓴 데이터를 신버전 리더가 읽을 수 있음컬럼 삭제 전 신버전 코드가 없는 컬럼을 허용
Full compatible양방향 모두 허용롤링 배포, 블루-그린 전환 시 필요

대부분의 데이터 파이프라인에서 가장 먼저 지켜야 할 기준은 backward compatible이다. 구버전 컨슈머가 신버전 데이터를 처리할 수 있어야 새 프로듀서를 먼저 배포할 수 있다.

안전한 변경 vs 위험한 변경

✅ 안전한 변경 (backward compatible)
- 선택적(nullable) 컬럼 추가
- 새로운 enum 값 추가 (컨슈머가 unknown 허용 시)
- 컬럼에 기본값 추가
- 새 테이블/토픽 추가

❌ 위험한 변경 (breaking change)
- 컬럼 삭제
- 컬럼 이름 변경
- 컬럼 타입 변경 (string → int 등)
- NOT NULL 제약 추가 (기존 NULL 데이터 존재 시)
- 파티션 키 변경

위험한 변경을 피할 수 없다면 Expand-Contract 패턴을 써야 한다.


Expand-Contract 패턴

Expand-Contract(또는 Parallel Change)는 브레이킹 체인지를 다운타임 없이 적용하는 표준 패턴이다. 변경을 한 번에 하지 않고 세 단계로 나눠 배포한다.

① Expand (신규 컬럼 추가) ② Migrate (데이터·코드 이전) ③ Contract (구 컬럼 삭제) 테이블 스키마 user_name VARCHAR(100) full_name VARCHAR(200) ← 추가 두 컬럼 공존. 구 코드 계속 동작. 테이블 스키마 user_name VARCHAR(100) full_name VARCHAR(200) 신 코드: full_name 읽기·쓰기 우선 테이블 스키마 user_name full_name VARCHAR(200) 구 컬럼 삭제. 신 코드만 존재. App 버전 v1 (구 코드) user_name 만 읽고 씀 v2 (이전 코드) full_name 우선, user_name 병행 쓰기 v3 (신 코드) full_name 만 읽고 씀 배포 1 배포 2 롤백: DB 마이그레이션 취소만 롤백: v1 재배포 → user_name 계속 쓰임 컬럼 삭제 후엔 롤백 복잡 — 신중하게 각 단계는 독립적으로 배포·검증·롤백 가능하다
Expand-Contract 패턴: 컬럼 이름 변경 예시

실제 마이그레이션 스크립트: Expand 단계

-- ① Expand: full_name 컬럼 추가 (nullable, 즉시 적용 가능)
ALTER TABLE users ADD COLUMN full_name VARCHAR(200) DEFAULT NULL;

-- 기존 데이터 백필 (대규모 테이블이면 배치로 나눠 실행)
UPDATE users
SET full_name = user_name
WHERE full_name IS NULL
  AND id BETWEEN 1 AND 100000;
-- ... 이후 배치를 잡으로 나눠 실행

Migrate 단계: 양쪽 쓰기

# 애플리케이션 코드 v2: 두 컬럼 모두 씀
def create_user(name: str) -> dict:
    conn.execute(
        "INSERT INTO users (user_name, full_name) VALUES (%s, %s)",
        (name, name)  # 두 컬럼 모두 동일 값으로 쓴다
    )

Contract 단계: 구 컬럼 삭제

-- ③ Contract: 모든 클라이언트가 v3 로 전환된 뒤에만 실행
-- PostgreSQL: Online DDL이 아닌 경우 락 주의
ALTER TABLE users DROP COLUMN user_name;

MySQL에서는 pt-online-schema-change 또는 gh-ost를 사용해 대형 테이블의 컬럼 삭제를 무중단으로 진행한다. 자세한 내용은 mysql-advanced-operations 시리즈 3편을 참고한다.


Kafka/Avro Schema Registry 호환성

스트리밍 파이프라인에서는 Schema Registry가 브레이킹 체인지를 방어하는 핵심 장치다. Confluent Schema Registry는 네 가지 호환성 레벨을 제공한다.

레벨허용금지권장 대상
BACKWARD필드 추가(default 있음), 삭제(optional)필수 필드 추가, 타입 변경컨슈머 먼저 배포할 때
FORWARD필드 삭제신규 필드 필수 지정프로듀서 먼저 배포할 때
FULL선택적 필드 추가·삭제만모든 타입 변경가장 엄격; 운영 안정성 중시
NONE모든 변경개발 초기 실험 환경

대부분의 운영 환경에서는 FULL 또는 BACKWARD를 설정하고, CI 파이프라인에서 Schema Registry에 schema를 등록 시도해 호환성 검사를 자동화한다.

# CI에서 스키마 호환성 사전 검사
# (실제 등록하지 않고 호환성만 확인)
curl -X POST \
  http://schema-registry:8081/compatibility/subjects/orders-value/versions/latest \
  -H "Content-Type: application/vnd.schemaregistry.v1+json" \
  -d '{"schema": "{\"type\":\"record\",\"name\":\"Order\",\"fields\":[{\"name\":\"id\",\"type\":\"string\"},{\"name\":\"amount\",\"type\":\"double\"},{\"name\":\"currency\",\"type\":[\"null\",\"string\"],\"default\":null}]}"}'

# 응답이 {"is_compatible":true} 이면 안전

Data Contract 검증을 CI에 통합하기

Data Contract는 프로듀서와 컨슈머 사이의 명시적 합의다. 스키마 호환성 검사에 더해 SLA, 품질 기대치, 필드 의미론까지 포함한다. Data Contract CLI는 이를 CI 파이프라인에 통합하는 도구다.

# datacontract.yaml
dataContractSpecification: 0.9.3
id: urn:datacontract:orders:v2
info:
  title: Orders Contract
  version: 2.0.0
  owner: data-platform-team

models:
  orders:
    fields:
      id:
        type: string
        required: true
      amount:
        type: double
        required: true
      currency:
        type: string
        required: false
        default: "KRW"

quality:
  type: SodaCL
  specification:
    checks for orders:
      - row_count > 0
      - missing_count(id) = 0
      - duplicate_count(id) = 0
# .github/workflows/contract-check.yml
- name: Data Contract 호환성 검사
  run: |
    pip install datacontract-cli
    # 현재 계약과 이전 버전 비교
    datacontract diff \
      --from datacontract-v1.yaml \
      --to datacontract.yaml
    # 브레이킹 체인지가 있으면 비-0 exit code 반환 → CI 실패

브레이킹 체인지 감지 체크리스트

필드 삭제 여부          → datacontract diff 또는 Schema Registry 호환성 검사
필드 타입 변경 여부     → 동일
NOT NULL 제약 추가 여부 → Flyway/Liquibase changelog 리뷰
파티션 키 변경 여부     → 수동 리뷰 필수 (dbt, Iceberg 모두 해당)
dbt source 컬럼 변경    → dbt test --select source: 실행 후 확인

dbt 스키마 변경 안전 가이드

dbt 모델의 스키마가 바뀔 때는 sources.ymlschema.yml의 계약을 먼저 업데이트하고, CI에서 테스트를 실행해 하위 모델이 영향을 받는지 확인한다.

# 변경된 모델과 하위 의존 모델만 테스트 (Slim CI)
dbt test --select state:modified+ --defer --state ./prod-artifacts

컬럼을 삭제하거나 이름을 바꿀 때는 두 버전 공존 기간을 반드시 만든다.

-- dbt 모델에서 컬럼 이름 변경: Expand 단계
-- models/orders.sql
SELECT
    id,
    customer_id,
    total_amount,
    total_amount AS order_total  -- 새 이름 추가, 구 이름 유지
FROM {{ ref('stg_orders') }}

모든 하위 모델이 order_total로 이전된 것을 확인한 뒤에야 total_amount를 제거한다.


Flyway와 Liquibase: 마이그레이션 버전 관리

관계형 DB의 스키마 변경은 마이그레이션 도구로 버전을 관리해야 한다.

Flyway 마이그레이션 파일 규칙:
V{버전}__{설명}.sql

예시:
V1__init_users_table.sql
V2__add_full_name_column.sql
V3__drop_user_name_column.sql   ← Contract 단계에서만 생성
# CI에서 Flyway 검증
- name: Flyway migrate (dry-run)
  run: |
    flyway \
      -url=jdbc:postgresql://localhost:5432/testdb \
      -user=ci_user \
      -password=${{ secrets.DB_PASSWORD }} \
      -dryRunOutput=migration-plan.sql \
      migrate
    cat migration-plan.sql

dryRunOutput으로 실제 실행 전에 어떤 SQL이 수행될지 미리 출력해 리뷰할 수 있다. CI에서 이 파일을 PR 아티팩트로 저장하면 DBA가 리뷰하기 편리하다.


롤백 계획

스키마 변경의 롤백은 애플리케이션 롤백보다 복잡하다. Expand-Contract 패턴을 쓰면 각 단계별 롤백이 가능하다.

단계롤백 방법복잡도
Expand 취소추가한 컬럼 DROP (데이터 없으면 간단)낮음
Migrate 취소구 코드 재배포 + 구 컬럼으로 복귀중간
Contract 취소삭제한 컬럼 복원 불가 (백업에서 복구 필요)높음

Contract 단계 진입 전에 반드시 스냅샷 백업을 찍는다. 스냅샷 없이 컬럼을 삭제하면 PITR로도 복구에 수십 분이 소요된다.


References

  • Pete Hodgson, "Expand/Contract: making a breaking change without a big bang": https://blog.thepete.net/blog/2023/12/05/expand/contract-making-a-breaking-change-without-a-big-bang/
  • datasops Blog, "Database Migrations Without Downtime — Expand-Contract, Shadow Tables, and Feature Flags": https://www.datasops.com/blog/database-migrations-zero-downtime
  • Confluent Docs, "Schema Registry Data Contracts | Migration Rules, Quality Validation": https://docs.confluent.io/platform/current/schema-registry/fundamentals/data-contracts.html
  • Conduktor, "Schema Evolution: 8 Kafka Best Practices": https://www.conduktor.io/glossary/schema-evolution-best-practices
  • PlanetScale Blog, "Backward compatible database changes": https://planetscale.com/blog/backward-compatible-databases-changes
  • Harness Blog, "Database Schema Evolution: Designing for Continuous Change": https://www.harness.io/blog/database-schema-evolution-designing-for-continuous-change
  • datasops Blog, "Data Contracts in Practice — Schema Versioning, Evolution, and Producer-Consumer Agreements": https://www.datasops.com/blog/data-contracts-versioning
  • Flyway Documentation: https://documentation.red-gate.com/flyway