스키마 변경 안전 배포: 하위 호환, 마이그레이션, 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 단계
-- ① 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.yml과 schema.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.sqldryRunOutput으로 실제 실행 전에 어떤 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