이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- 무중단 DB 마이그레이션은 어떤 순서로 설계해야 하는가?
- 컬럼 추가, backfill, 컬럼 제거는 왜 한 번에 하면 위험한가?
- 운영 마이그레이션에서 인프라/DBA와 무엇을 확인해야 하는가?
개요
DB 마이그레이션은 운영 데이터를 가진 시스템을 변화시키는 작업이다. 애플리케이션 코드는 롤링 배포로 여러 버전이 동시에 떠 있을 수 있고, DB 스키마는 모든 버전이 공유한다. 그래서 DB 변경은 코드 변경보다 더 보수적으로 설계해야 한다.
무중단 배포의 핵심은 호환성이다. 구버전 코드와 신버전 코드가 동시에 동작해도 깨지지 않는 순서로 변경해야 한다.
원리
무중단 마이그레이션의 원리는 코드, 스키마, 데이터가 동시에 바뀐다고 가정하지 않는 것이다. 롤링 배포, replica 지연, batch, 캐시, 메시지 소비자는 서로 다른 속도로 변하므로 각 단계가 이전 단계와 다음 단계를 모두 견뎌야 한다.
좋은 마이그레이션은 SQL 파일 하나가 아니라 배포 순서, 호환 코드, backfill, 검증, 중단 기준, 정리 계획으로 구성된다. 특히 삭제와 rename은 마지막에 해야 하며, “되돌리기”보다 “앞으로 고치기”가 더 안전한 경우가 많다.
상황
members.name을 first_name, last_name으로 분리해야 한다.
나쁜 접근은 한 번에 컬럼을 바꾸고 코드를 배포하는 것이다.
ALTER TABLE members DROP COLUMN name;
ALTER TABLE members ADD COLUMN first_name varchar(100);
ALTER TABLE members ADD COLUMN last_name varchar(100);롤링 배포 중 구버전 서버가 아직 name을 읽거나 쓰면 즉시 실패한다.
Expand and Contract
무중단 변경은 보통 expand and contract로 나눈다.
- 새 컬럼을 nullable로 추가한다.
- 신버전 코드가 새 컬럼과 기존 컬럼을 함께 다룬다.
- 기존 데이터를 backfill한다.
- 읽기 경로를 새 컬럼으로 전환한다.
- 충분히 검증한 뒤 기존 컬럼을 제거한다.
ALTER TABLE members ADD COLUMN first_name varchar(100);
ALTER TABLE members ADD COLUMN last_name varchar(100);Backfill은 작은 배치로 수행한다.
UPDATE members
SET first_name = split_part(name, ' ', 1),
last_name = split_part(name, ' ', 2)
WHERE id > :last_id
AND id <= :next_id
AND first_name IS NULL;실제 이름 분리는 훨씬 복잡할 수 있다. 중요한 것은 재시작 가능하고 검증 가능한 배치로 설계하는 것이다.
먼저 확인할 것
- 구버전 코드와 신버전 코드가 동시에 떠도 되는가?
- DDL이 lock을 얼마나 잡는가?
- 대형 테이블에서 table rewrite가 발생하는가?
- backfill은 얼마나 오래 걸리는가?
- replica lag를 만들 수 있는가?
- 롤백은 스키마 롤백인가, forward fix인가?
- 배포 전후 검증 쿼리는 무엇인가?
운영 작업 전에는 다음 runbook을 만든다.
목표: members.name을 first_name/last_name으로 분리
배포 순서: expand -> dual write -> backfill -> read switch -> cleanup
예상 대상: members 3,200만 row
중단 기준: replica lag 10초 초과, lock wait 5초 초과, API p95 2배 증가
검증: first_name is null count, 샘플 회원 상세 API, error log
담당: backend A, DBA B, on-call C
복구: cleanup 전에는 old column 유지, cleanup 후에는 PITR/부분 복원 필요이 runbook이 있어야 인프라/DBA가 “무엇을 감시해야 하는지” 알 수 있다.
Backfill 설계
Backfill은 운영 장애를 자주 만든다. 작은 배치, 중단 가능성, 진행률 기록이 필요하다.
while (true) {
List<MemberNameRow> rows = memberRepository.findRowsToBackfill(lastId, 1000);
if (rows.isEmpty()) {
break;
}
memberRepository.updateNameParts(rows);
lastId = rows.get(rows.size() - 1).id();
Thread.sleep(100);
}이 코드는 단순 예시다. 운영에서는 실패 지점 저장, 중복 실행 안전성, 속도 조절, 모니터링, 중단 명령이 필요하다.
호환 코드는 반드시 제거 계획까지 가진다.
임시 코드:
- name과 first_name/last_name 모두 write
- read는 first_name이 있으면 새 컬럼, 없으면 name fallback
제거 조건:
- backfill 100%
- 7일간 fallback read 0건
- batch/admin/report에서 name 참조 0건
- cleanup migration 승인임시 코드를 영구 코드로 방치하면 다음 migration이 더 어려워진다.
인프라/DBA와 같이 볼 것
- DB 버전에서 해당 DDL이 online인지
- lock timeout과 statement timeout 설정
- replica lag 모니터링
- 백업 상태
- 배포 시간대
- 롤백 또는 forward fix 시나리오
- migration 도구 권한
인프라 입장에서는 DDL 하나가 DB 전체를 멈출 수 있다. 개발자가 “컬럼 하나 추가”라고 말해도 대형 테이블에서는 rewrite와 lock이 문제일 수 있다.
하지 말아야 할 대응
- 코드 배포와 컬럼 제거를 한 PR에 묶는다.
- 대형 테이블 backfill을 한 트랜잭션으로 실행한다.
- 운영에서 실행 시간과 lock 영향 없이 ALTER를 실행한다.
- down migration만 있으면 롤백 가능하다고 믿는다.
- 배포 중 구버전 서버 존재를 무시한다.
실전 팁
- 스키마 확장, 데이터 backfill, 읽기 전환, 삭제를 서로 다른 단계로 나눈다.
- 대형 DDL은 staging에서 시간과 lock을 먼저 측정한다.
- backfill은 재시작 가능하고 중단 가능한 작업으로 만든다.
- 구버전 애플리케이션이 새 스키마에서 동작하는지 확인한다.
- migration 후 검증 쿼리와 모니터링 항목을 미리 정한다.
- 임시 호환 코드에는 제거 조건과 제거 이슈를 반드시 붙인다.
- cleanup 전에는 batch, admin, report, BI, 수동 운영 쿼리의 old column 사용 여부를 확인한다.
- DDL 실행 전에는 같은 시간대 백업, 대량 batch, analytics job과 겹치지 않는지 확인한다.
주니어 팁
마이그레이션을 작성할 때는 “이전 코드도 이 스키마에서 동작하는가?”를 질문한다. 새 코드만 생각하면 롤링 배포 중 장애가 난다.
시니어 팁
시니어는 DB migration을 배포 계획으로 쪼갠다.
- 배포 1: 스키마 확장
- 배포 2: 양쪽 쓰기와 backfill
- 배포 3: 읽기 전환
- 배포 4: 옛 경로 제거
작업 수가 늘어 귀찮아 보여도 운영 리스크는 크게 줄어든다.
Guru급 팁
무중단 migration의 본질은 “한순간에 모든 것이 바뀐다”는 가정을 버리는 것이다. 코드 버전, DB 스키마, 데이터 내용, 캐시, 메시지 소비자는 서로 다른 속도로 변한다. 좋은 설계는 이 비동시성을 받아들인다.
위험 신호!
- 컬럼 rename이나 drop을 한 번에 배포한다.
- backfill 진행률과 재시작 지점이 없다.
- online DDL 지원 여부를 확인하지 않는다.
- down migration만 보고 안전하다고 판단한다.
- migration 중 replica lag와 lock wait를 보지 않는다.
- dual write 임시 코드가 영구화되어 데이터 불일치 원인이 된다.
- cleanup migration이 기능 배포와 같은 시간에 진행된다.
- 검증 쿼리 없이 “에러가 없으니 성공”으로 판단한다.
확인 질문
- Expand and contract가 필요한 이유는 무엇인가?
- 구버전과 신버전 애플리케이션이 동시에 동작해도 DB 스키마 호환성을 유지하기 위해서다.
- Backfill을 작은 배치로 해야 하는 이유는 무엇인가?
- 긴 트랜잭션, 락, I/O, replica lag, 롤백 부담을 줄이고 중단과 재시작을 가능하게 하기 위해서다.
- DB migration에서 down 스크립트만 믿으면 안 되는 이유는 무엇인가?
- 데이터 변환이나 삭제는 되돌리기 어렵고, 운영에서는 forward fix가 더 안전한 경우가 많기 때문이다.
- 임시 호환 코드를 제거하기 전에 확인해야 할 것은 무엇인가?
- backfill 완료, fallback 사용 0건, old column 참조 제거, batch/admin/report 영향 제거, cleanup migration 승인 여부다.