순간에 바뀌지 않는다

무중단 배포에서 코드가 한 번에 교체되는 일은 없다. 인스턴스가 하나씩 교체되는 동안 구 버전과 신 버전이 같은 DB를 함께 쓴다.

롤백하면 신 버전이 만든 데이터 위에서 구 버전이 다시 돈다.

그래서 스키마 변경은 “바꾼 뒤에 잘 도는가”만으로 부족하다. 바꾸는 도중과 되돌린 뒤에도 도는가를 함께 봐야 한다.

한 번에 하면 안 되는 것

ALTER TABLE users RENAME COLUMN name TO full_name;

배포 순간 구 버전 인스턴스들이 전부 name을 찾다 실패한다. 롤백해도 이번엔 컬럼이 없어서 계속 실패한다. 되돌릴 수 없는 상태에 들어간다.

컬럼 삭제, 이름 변경, NOT NULL 추가, 타입 축소가 여기 해당한다.

expand-contract

파괴적 변경을 여러 단계의 안전한 변경으로 쪼갠다.

1. 확장 (expand) — 새 컬럼을 추가한다. 기존 컬럼은 그대로 둔다

ALTER TABLE users ADD COLUMN full_name VARCHAR(255) NULL;

nullable로 추가하는 게 핵심이다. 구 버전은 이 컬럼을 모르지만 아무 영향이 없다.

2. 양쪽 쓰기 — 새 코드가 두 컬럼 모두에 쓴다. 읽기는 아직 옛 컬럼에서

이 상태로 배포한다. 롤백해도 구 버전은 옛 컬럼을 그대로 쓰므로 안전하다.

3. 백필 — 기존 데이터를 새 컬럼으로 옮긴다

한 번에 UPDATE하면 테이블 전체가 잠긴다. 배치로 나눠 돌린다.

UPDATE users SET full_name = name
WHERE full_name IS NULL LIMIT 1000;

4. 읽기 전환 — 새 컬럼에서 읽도록 배포한다. 쓰기는 아직 양쪽에

5. 축소 (contract) — 옛 컬럼 쓰기를 멈추고, 충분히 지난 뒤 컬럼을 삭제한다

각 단계는 직전 상태로 되돌릴 수 있다. 이게 이 방식의 전부다.

그 외 주의할 것

인덱스 추가. 큰 테이블에 인덱스를 걸면 그동안 쓰기가 막힐 수 있다. PostgreSQL은 CREATE INDEX CONCURRENTLY, MySQL 8은 온라인 DDL을 쓴다.

기본값 있는 컬럼 추가. 과거 버전에서는 테이블 전체를 다시 썼다. 최신 버전은 메타데이터만 바꾸지만, 쓰는 DB의 동작을 확인해야 한다.

마이그레이션은 배포와 분리한다. 스키마 변경을 애플리케이션 기동 시점에 실행하면, 인스턴스 여러 개가 동시에 시작할 때 충돌한다.

정리