이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Zero downtime schema change는 왜 여러 단계가 필요한가?
- 대형 테이블 DDL에서 어떤 lock과 rewrite 위험을 확인해야 하는가?
- 구버전과 신버전 애플리케이션이 동시에 떠 있는 상황을 어떻게 고려하는가?
개요
Zero downtime DDL은 DB 스키마를 바꾸면서 사용자 요청을 중단하지 않는 변경 방식이다. 핵심은 호환성이다. 롤링 배포 중에는 구버전과 신버전 애플리케이션이 같은 DB를 동시에 사용할 수 있다.
무중단 변경은 한 번에 끝내기보다 expand and contract로 나눈다.
원리
컬럼 이름 변경을 예로 들자. 한 번에 rename하면 구버전 코드가 깨질 수 있다.
ALTER TABLE members RENAME COLUMN name TO full_name;더 안전한 방식은 새 컬럼을 추가하고, 양쪽을 호환시키고, 데이터를 옮기고, 충분히 검증한 뒤 제거하는 것이다.
1. full_name nullable 추가
2. 애플리케이션이 name과 full_name을 함께 처리
3. backfill
4. 읽기 경로를 full_name으로 전환
5. name 쓰기 중단
6. name 제거이 순서는 빠르게 보이지만 실제로는 여러 배포와 운영 검증으로 나뉜다.
배포 A: full_name 추가, 기존 코드는 영향 없음
배포 B: 쓰기 시 name과 full_name을 함께 기록
운영 작업: backfill과 검증
배포 C: 읽기 기준을 full_name으로 전환
관찰 기간: old/new 서버, batch, admin 모두 정상 확인
배포 D: name 참조 제거
운영 작업: name 제거롤링 배포에서는 배포 B와 C 사이에 구버전 서버가 살아 있을 수 있다. 그래서 새 스키마는 이전 코드와도 호환되어야 하고, 새 코드는 이전 데이터와도 호환되어야 한다.
DDL 위험 확인
DB별로 DDL 동작이 다르다. 다음을 확인해야 한다.
- table lock이 걸리는가?
- metadata lock이 걸리는가?
- table rewrite가 발생하는가?
- online DDL이 가능한가?
- replica에 어떤 영향을 주는가?
- 실패하면 중단 가능한가?
PostgreSQL, MySQL 모두 버전이 올라가며 DDL 개선이 있었지만, 모든 변경이 안전한 것은 아니다. “컬럼 하나 추가”도 DB 버전과 조건에 따라 위험도가 다르다.
예를 들어 PostgreSQL은 버전에 따라 constant default 컬럼 추가가 최적화될 수 있지만, 모든 default와 모든 ALTER가 안전한 것은 아니다. MySQL도 ALGORITHM=INPLACE, INSTANT, COPY, LOCK 옵션과 실제 지원 범위가 변경 종류와 버전에 따라 다르다. 운영 runbook에는 “우리 DB 버전에서 이 SQL이 어떤 알고리즘과 lock으로 실행되는가”가 들어가야 한다.
호환 코드
새 컬럼을 도입할 때 애플리케이션은 중간 상태를 견뎌야 한다.
public String displayName(Member member) {
if (member.getFullName() != null) {
return member.getFullName();
}
return member.getName();
}이런 호환 코드는 임시 코드다. 마이그레이션이 끝나면 제거 계획까지 있어야 한다.
인덱스 생성
대형 테이블 인덱스 생성은 운영에 큰 영향을 줄 수 있다. PostgreSQL에서는 CREATE INDEX CONCURRENTLY가 유용할 수 있지만 트랜잭션 블록 제약과 실패 시 invalid index 처리 등을 알아야 한다.
CREATE INDEX CONCURRENTLY idx_orders_member_created_id
ON orders (member_id, created_at DESC, id DESC);MySQL에서는 online DDL 지원 범위를 확인한다.
무중단 인덱스 생성도 “부하 없음”은 아니다. 읽기/쓰기와 병행할 수 있더라도 CPU, I/O, WAL/binlog, replica lag가 늘 수 있다. 인프라 담당자와 다음 기준을 맞춘다.
- 실행 시간대
- lock wait 허용치
- replica lag 중단 기준
- DB CPU/I/O 임계치
- 실패 시 invalid index나 partially applied 상태 처리
- 같은 시간대 batch와 백업 작업 여부
배포 호환성 테스트
무중단 DDL은 SQL 테스트만으로 끝나지 않는다. 다음 조합을 확인해야 한다.
| 코드 버전 | 스키마 상태 | 기대 |
|---|---|---|
| old app | old schema | 기존 정상 |
| old app | expanded schema | 기존 정상 |
| new app | expanded schema with old data | null/old 데이터 호환 |
| new app | backfilled schema | 새 경로 정상 |
| new app | contracted schema | old 컬럼 미사용 |
이 조합을 생각하지 않으면 staging에서는 성공했지만 롤링 배포 중 특정 서버만 실패하는 일이 생긴다.
실전 팁
- 스키마 변경과 코드 변경을 여러 배포로 나눈다.
- 구버전 코드가 새 스키마에서 동작하는지 확인한다.
- 삭제는 가장 마지막에 한다.
- 대형 테이블 DDL은 staging에서 시간과 lock을 측정한다.
- DDL 전후로 slow query, lock wait, replica lag를 모니터링한다.
- 새 코드가 old data를 읽고 old 코드가 expanded schema에서 동작하는지 테스트한다.
- DDL에는 가능하면 lock timeout과 중단 기준을 둔다.
- 인덱스 생성은 완료 여부뿐 아니라 query plan이 실제로 바뀌었는지 확인한다.
위험 신호!
- rename/drop을 코드 배포와 동시에 한다.
- 롤링 배포 중 구버전 서버를 고려하지 않는다.
- 대형 테이블 인덱스를 트래픽 피크에 생성한다.
- online DDL 지원 여부를 확인하지 않는다.
- 임시 호환 코드 제거 계획이 없다.
- 무중단 변경이라고 하면서 old app과 new schema 조합을 테스트하지 않는다.
- 인프라 작업 시간대에 backfill, index build, backup, analytics job이 겹친다.
- DDL 성공만 보고 replica lag와 쿼리 계획 변화를 보지 않는다.
확인 질문
- Zero downtime schema change가 여러 단계로 나뉘는 이유는 무엇인가?
- 구버전과 신버전 코드가 동시에 동작하는 동안 DB 스키마 호환성을 유지하기 위해서다.
- DDL 실행 전 확인해야 할 운영 위험은 무엇인가?
- lock, table rewrite, online DDL 지원 여부, replica lag, 실행 시간, 실패 시 중단 가능성이다.
- 삭제 작업을 마지막에 해야 하는 이유는 무엇인가?
- 아직 구버전 코드나 배치가 해당 컬럼을 사용할 수 있으므로 충분한 전환과 검증 후 제거해야 하기 때문이다.
- 무중단 DDL에서 old app과 expanded schema 조합을 확인해야 하는 이유는 무엇인가?
- 롤링 배포 중 구버전 서버가 새 스키마를 사용하는 시간이 생길 수 있으므로 이전 코드가 깨지지 않아야 하기 때문이다.