取り消せるスキーマ変更
한국어 원문으로 표시합니다.
한 줄 요약
가역성은 코드의 속성이 아니라 코드와 데이터와 시간의 조합이다 — 같은 변경이라도 아무도 안 썼으면 되돌릴 수 있고, 백만 건이 쌓인 뒤에는 되돌릴 수 없다.
왜 이게 문제인가
"down 스크립트가 있으니 안전하다" 는 말이 위험한 이유가 여기 있다. DROP COLUMN 을 되돌리는 down 은 컬럼 구조만 되살릴 뿐 그 안에 있던 값은 못 되살린다. 스키마는 되돌아가고 데이터는 안 돌아온다.
그래서 롤백 계획을 세울 때 물어야 할 것은 "되돌리는 스크립트가 있는가" 가 아니라 "되돌린 뒤에 잃는 것이 무엇인가" 다. 이 질문을 배포 전에 하면 대개 확장 → 이관 → 축소로 나눠 가는 쪽을 고르게 된다.
가역성은 코드의 속성이 아니다
"이 변경은 롤백 가능한가?"라는 질문에 대한 정확한 답은 이렇다.
가역성은 코드와 데이터와 시간의 조합이다. 같은 코드 변경이라도 아직 아무도 쓰지 않았으면 가역이고, 백만 건이 쌓인 뒤에는 비가역이다.
컬럼을 하나 추가했다. 되돌릴 수 있는가? 어제 배포했고 아무도 안 썼다면 그렇다. 일주일 동안 백만 건이 그 컬럼에 값을 채웠다면, DROP 은 그 백만 건을 버리는 일이다. 스키마는 되돌아가지만 데이터는 안 돌아온다.
"down 스크립트가 있으니 안전하다"는 착각이 여기서 나온다.
DROP COLUMN 을 되돌리는 down 은 컬럼 구조만 되살릴 뿐 값은 못 되살린다.
그래서 이런 말이 있다 — 다운 마이그레이션은 대개 거짓말이다.
위험한 DDL 목록
| DDL | 왜 위험한가 |
|---|---|
NOT NULL 추가 |
전체 행을 검사하느라 장시간 잠금. 기존 NULL 이 있으면 실패 |
| 컬럼 이름 변경 | 원자적으로 보이지만 사실상 삭제 + 추가. 구버전 코드가 즉시 깨진다 |
| 타입 변경 | DBMS 에 따라 테이블 재작성. 대용량이면 매우 오래 걸린다 |
| 인덱스 생성 | 대용량 테이블에서 잠금 또는 부하. 온라인 옵션 확인 필요 |
| 대량 백필 UPDATE | 거대한 트랜잭션 → 잠금·복제 지연. 읽기 복제본이 무너진다 |
DROP COLUMN |
데이터 소멸. 되돌릴 수 없다 |
특히 대량 백필을 조심해야 한다. 500만 행을 한 트랜잭션으로 UPDATE 하면 트랜잭션 로그가 폭증하고 복제 지연이 생긴다. 조회 트래픽이 복제본으로 가는 구성이면, 이 순간 조회 서비스가 오래된 데이터를 보여 준다. 그래서 백필은 반드시 배치로 쪼갠다.
-- 나쁜 예
UPDATE ORDERS SET DLVR_STS = '01' WHERE DLVR_STS IS NULL;
-- 좋은 예: 1000건씩, 사이에 잠깐 쉬면서
UPDATE ORDERS SET DLVR_STS = '01'
WHERE ORD_NO IN (SELECT ORD_NO FROM ORDERS WHERE DLVR_STS IS NULL LIMIT 1000);
-- 영향 행이 0이 될 때까지 반복
확장 → 이관 → 축소 (Expand / Migrate / Contract)
되돌릴 수 있는 스키마 변경의 표준 패턴이다. 핵심 규칙: 한 번의 배포에 두 단계를 함께 넣지 않는다.
[1차 배포 — 확장]
새 컬럼 추가 (NULL 허용, 기본값 있음)
애플리케이션: 새 컬럼과 옛 컬럼에 모두 쓰고, 읽기는 옛 컬럼
→ 이 시점에서 롤백하면? 새 컬럼을 아무도 안 읽으니 안전
[2차 — 이관]
기존 데이터 백필 (배치로 쪼개서)
검증: 두 컬럼 값이 일치하는가
[3차 배포 — 전환]
애플리케이션: 읽기를 새 컬럼으로
→ 롤백하면 옛 컬럼을 읽는데, 계속 써 왔으니 값이 있다. 안전
[4차 배포 — 축소]
애플리케이션: 옛 컬럼 쓰기 중단
충분한 관찰 기간 후 옛 컬럼 DROP
→ 이 시점에서야 비가역이 된다
느려 보이지만, 각 단계마다 되돌릴 수 있다는 것이 이 패턴의 전부다. 컬럼 이름을 한 번에 바꾸는 것은 30초 걸리고, 잘못되면 서비스가 멈춘다. 이 패턴은 2주 걸리고, 어느 시점에도 멈추지 않는다.
변경관리 대장
SI 프로젝트에서 DDL 은 개발자가 마음대로 날리는 것이 아니다. 변경관리 대장을 두고, 운영 반영은 승인을 거친다.
| 항목 | 왜 필요한가 |
|---|---|
| 변경 ID / 일자 | 추적 단위 |
| 대상 객체 | 영향 범위 파악의 출발점 |
| DDL 스크립트 파일 | 실제로 실행할 것 |
| 롤백 스크립트 파일 | 없으면 승인 안 남 |
| 예상 소요 시간 | 서비스 중단 시간 산정 |
| 영향 시스템 | 연동 상대에게 통보해야 할 대상 |
| 승인자 | 책임 |
롤백 스크립트를 요구하는 것이 이 대장의 핵심 가치다. 롤백을 쓰다 보면 "이건 되돌릴 수 없네"를 배포 전에 깨닫게 된다. 그 깨달음이 확장-이관-축소로 설계를 바꾸게 만든다.
스키마 변경과 애플리케이션 배포의 순서
이것도 실수가 잦다.
- 컬럼 추가: DB 먼저 → 애플리케이션 나중. (앱이 없는 컬럼을 쓰면 안 되니까)
- 컬럼 삭제: 애플리케이션 먼저 → DB 나중. (앱이 안 쓰게 만든 뒤 지운다)
한 문장으로: 더하는 것은 DB 가 먼저, 빼는 것은 앱이 먼저.
그리고 롤링 배포 중에는 구버전과 신버전이 동시에 돈다. 그래서 스키마는 항상 양쪽 버전이 모두 동작하는 상태여야 한다. 이것이 확장-이관-축소가 필요한 근본 이유다.
블루그린은 스키마를 해결하지 않는다
무중단 배포 전략을 이야기할 때 반드시 짚어야 할 점.
컴퓨트는 복제하기 쉽지만 데이터베이스는 대개 공유한다. 그래서 블루그린은 애플리케이션 롤백을 초 단위로 만들어 주지만 스키마 문제는 전혀 해결하지 않는다.
블루와 그린이 같은 DB 를 본다면, 스키마는 양쪽 애플리케이션 버전과 모두 호환돼야 한다. 결국 같은 이야기로 돌아온다.
마이그레이션 검증
적용했으면 확인해야 한다. 4단계로 본다.
1. 스키마 컬럼·타입·제약·인덱스가 의도대로인가
2. 데이터 행 수 · 합계 · NULL 개수 · 체크섬이 보존됐는가
3. 성능 주요 쿼리의 실행계획과 응답시간이 나빠지지 않았는가
4. 앱 핵심 기능 스모크 테스트
2번의 체크섬이 특히 유용하다. 행 수와 합계만으로는 "한 건이 빠지고 다른 한 건이 두 배가 된" 상황을 못 잡는다.
-- 이관 전 기준선을 만들어 둔다
CREATE TABLE MIG_BASELINE AS
SELECT COUNT(*) AS CNT, SUM(ORD_AMT) AS AMT,
COUNT(DISTINCT CUST_ID) AS CUSTS FROM ORDERS;
-- 이관 후 같은 쿼리로 비교
작업 전에 기준선을 만들어 두는 것이 요령이다. 작업 후에 "원래 몇 건이었죠?"를 묻게 되면 이미 늦었다.
현장에서 만나는 모습
스키마 변경이 사고로 번지는 경로는 대개 둘이다.
첫째는 잠금이다. NOT NULL 을 추가하면 전체 행을 검사하느라 테이블이 잠기고, 그동안 그 테이블을 쓰는 모든 요청이 대기한다. 개발 DB 에서 0.2 초였던 것이 천만 건짜리 운영 테이블에서는 몇 분이 되고, 그 몇 분은 서비스 정지와 구분되지 않는다.
둘째는 순서다. 애플리케이션을 먼저 배포하면 아직 없는 컬럼을 읽다가 죽고, 스키마를 먼저 바꾸면 옛 애플리케이션이 새 제약을 위반한다. 그래서 "둘 다 동시에 배포" 는 계획이 아니다 — 어느 쪽이 먼저여도 견디도록 만들어 두는 것이 계획이다.
블루그린으로 이걸 해결하려는 시도도 자주 보는데, 두 색이 같은 DB 를 본다는 사실은 바뀌지 않는다. 애플리케이션은 무중단으로 바뀌어도 스키마는 그대로 한 벌이다.