컬럼 이름 하나 바꾸고 배포했는데 5분간 락이 걸린 적이 있다면, “마이그레이션 = SQL 한 방”이 이미 깨진 겁니다. 무중단 스키마 변경은 앱 코드와 DB 변경을 같은 배포 단위로 묶지 않는 일부터 시작합니다. 확장(expand) → 이중 쓰기·읽기 → 축소(contract) 순서를 지키면, 트래픽이 있는 서비스에서도 롤백 창을 남길 수 있습니다.
아래는 백엔드·데브옵스·풀스택이 PR 전에 그대로 붙일 수 있는 점검 목록입니다. 배포 공통 게이트는 프로덕션 배포 체크리스트를, 단계 공개가 필요하면 기능 플래그 롤아웃을 같이 보세요.
모든 ALTER를 무중단으로 가져가면 비용이 커집니다. 먼저 표로 갈라 두고, 팀에 “이 변경은 어느 줄인가”만 합의해도 야간 장애가 줄어듭니다.
| 상황 | 권장 | 이유 |
| 24/7 API·결제·로그인 트래픽 | 무중단(확장-축소) | 짧은 락도 타임아웃·재시도 폭주로 이어짐 |
| 내부 배치·야간 전용 잡 | 점검 창 가능 | 사용자 영향 적으면 단순 마이그레이션이 안전 |
| 테이블 수억 행·핫 파티션 | 무중단 + 온라인 도구 | 풀 테이블 리라이트가 수분~수시간 가능 |
| 개발/스테이징 전용 DB | 단순 적용 | 속도·가독성 우선, 패턴 연습용 |
| 파괴적 데이터 삭제·PII 정리 | 별도 런북 | 스키마 이슈가 아니라 데이터 수명 이슈 |
마틴 파울러의 진화형 데이터베이스 설계(Evolutionary Database Design)는 “스키마와 코드를 작은 단위로, 항상 호환 가능하게” 맞추라고 말합니다. 한 스프린트에 rename·타입 변경·NOT NULL·인덱스 재작성을 한꺼번에 넣으면 롤백이 거의 불가능해집니다.
무중단의 뼈대는 단순합니다. 넓히기 → 옮기기 → 좁히기. 각 단계는 별도 마이그레이션 파일·별도 배포로 나눕니다.
예: user_name → display_name 이름 변경. 잘못된 한 방: RENAME COLUMN 직후 앱 배포. 맞는 순서: (1) display_name 추가(nullable) (2) 앱이 둘 다 쓰기·우선 읽기 (3) 백필 완료 검증 (4) 앱은 display_name만 (5) 구 컬럼 삭제. 중간에 어디든 멈추면 서비스는 계속 돕니다.
SQL이 맞아도 디스크·권한·락 큐가 막히면 장애입니다. 스테이징에서 “같은 데이터 규모 근사치”로 한 번 돌린 기록이 없으면 프로덕션 금지에 가깝습니다.
ORM이 생성한 한 줄이 실제로는 테이블 재작성을 유발할 수 있습니다. PostgreSQL·MySQL 버전별로 다릅니다. “문서에 쓰인 빠른 경로”를 버전 단위로 확인하세요.
| 연산 | 흔한 사고 | 안전한 패턴 |
| 컬럼 RENAME | 구 앱이 컬럼 못 찾음 | 새 컬럼 추가 → 이중 쓰기 → 구 제거 |
| 타입 변경 (int→bigint 등) | 리라이트·검증 실패 | 새 컬럼 + 백필 + 전환. 가능하면 호환 타입만 |
| NOT NULL 추가 | 기존 NULL 행·긴 검증 | DEFAULT/백필 완료 후 제약. 검증을 배치로 |
| DEFAULT 변경 | 버전별 메타만/전체 갱신 차이 | 엔진 문서 확인. 대량 갱신이면 배치 |
| 인덱스 CREATE | 쓰기 블로킹 | CONCURRENTLY·온라인 DDL·유지보수 창 |
| FK 추가 | 검증 스캔·부모 락 | 데이터 정리 후 NOT VALID → VALIDATE 분리(지원 시) |
| DROP COLUMN/TABLE | 구 코드 500·복구 불가 | 코드 제거 배포 후, 관찰 기간 지나 삭제 |
스키마만 넓히고 데이터를 안 채우면 신 코드가 빈 값을 봅니다. 백필은 “한 번에 UPDATE 전체”가 아니라 배치·재개 가능·관측 가능이어야 합니다.
카나리 10%면 90%는 아직 구 바이너리입니다. 스키마가 구 코드를 깨는 순간, 그 90%가 에러 예산입니다.
인덱스 하나가 쓰기 경로를 멈출 수 있습니다. PostgreSQL의 CREATE INDEX CONCURRENTLY는 트랜잭션 블록 밖에서 돌고, 실패 시 invalid 인덱스가 남을 수 있습니다. MySQL·MariaDB는 online DDL·알고리즘 옵션을 버전별로 확인합니다.
축소(contract) 이후 DROP은 사실상 앞으로만 갑니다. 롤백 가능 구간을 배포 노트에 적지 않으면, 장애 때 회의만 늘어납니다.
도구는 파일 버전 관리·적용 이력·CI 연동을 대신해 줍니다. 무중단 패턴 자체는 도구가 자동으로 해주지 않습니다.
| 도구 | 잘 맞는 경우 | 주의 |
| Flyway / Liquibase | SQL 중심·다수 서비스 표준화 | 팀 규약 없으면 거대 스크립트화 |
| golang-migrate 등 CLI | 앱과 같은 저장소, 단순 up/down | down이 프로덕션에서 위험한 경우 비활성 |
| Prisma Migrate 등 ORM | 타입·스키마 동기화 | 자동 diff를 무중단 단위로 쪼개야 함 |
| gh-ost / pt-osc 등 | MySQL 대용량 온라인 변경 | 운영 러닝커브·컷오버 리허설 필요 |
의존성 봇이 ORM 메이저 업을 올리면 마이그레이션 동작이 바뀔 수 있습니다. 대규모 흡수 주는 의존성 업데이트 체크리스트와 DDL 주를 겹치지 마세요.
- 분류 — 표에서 무중단 vs 점검 창. 위험 연산이면 확장-축소 강제.
- Expand PR — 추가만. 롤백 노트·예상 락·스테이징 dry-run 결과 첨부.
- 앱 호환 배포 — 구·신 스키마 모두 동작. 필요 시 플래그 기본 off.
- 백필 — 배치·재개·diff 알람. 완료 정의 충족 시까지 축소 금지.
- 읽기/쓰기 전환 — 신 경로 비율 승급. 에러·지연 가드레일.
- Contract PR — 구 코드 제거 배포 후, 관찰 기간 지나 컬럼/테이블 삭제.
- 회고 한 줄 — 예상 밖 락·diff·타임아웃을 체크리스트에 환원.
결제·권한처럼 잘못되면 돈이 움직이는 스키마는 단계를 더 쪼개고, 리뷰어를 두 명 두는 편이 낫습니다. “금요일 오후 Expand+Contract 한 방”은 체크리스트 통과로 보지 마세요.
점검 창 10분이면 무중단 패턴이 필요 없나요?
트래픽이 없고 롤백이 단순하면 점검 창이 더 안전할 수 있습니다. 다만 창이 “항상 10분”이 아니고, 테이블이 커질수록 창이 길어집니다. 성장 중인 서비스는 지금 단순 적용이 되더라도, rename·NOT NULL 같은 연산부터 확장-축소 습관을 들이는 편이 이후 비용이 작습니다.
Prisma나 ORM 자동 마이그레이션만으로 충분한가요?
이력 관리·로컬 동기화에는 충분합니다. 프로덕션 무중단은 자동 diff를 사람이 쪼개고, 백필·이중 쓰기·관찰을 추가해야 합니다. 자동 생성 SQL에 RENAME·DROP이 있으면 그대로 머지하지 마세요.
down 마이그레이션을 프로덕션에서 실행해도 되나요?
데이터 손실·재작성 위험이 있으면 비추천입니다. 많은 팀이 프로덕션 down을 막고, 앞으로 가는 보정 마이그레이션(forward fix)만 허용합니다. down이 필요하다면 스테이징 복원 드릴과 동일한 등급의 리뷰를 요구하세요.
컬럼 추가만 하는데도 락이 걸리나요?
엔진·버전·기본값·NOT NULL 여부에 따라 다릅니다. “빠른 메타데이터만 바꾸는 경로”와 “테이블 재작성”이 갈립니다. 사용 중인 DB 메이저 버전 문서를 보고, 스테이징에서 락 대기 시간을 재는 것이 안전합니다.
백필이 며칠 걸리는데 배포를 멈춰야 하나요?
Expand와 호환 앱은 먼저 배포한 뒤, 백필만 길게 돌리면 됩니다. 그 기간 동안 이중 쓰기·불일치 알람이 동작해야 합니다. 백필 중에 Contract(삭제)만 하지 않으면 서비스는 계속 배포할 수 있습니다.
멀티 테넌트·샤딩이면 순서가 달라지나요?
샤드마다 적용 시각이 어긋날 수 있어, 앱은 더 오래 “구·신 스키마 모두” 지원해야 합니다. 테넌트 단위 카나리, 샤드 버전 표, 실패 샤드만 재시도하는 런북을 추가하세요. 전 샤드 일괄 Contract는 마지막에 둡니다.