도입
컬럼을 한 번에 바꾸면 이전 애플리케이션과 새 애플리케이션이 동시에 동작하는 배포 구간에서 한쪽이 실패할 수 있다. Expand-Contract는 호환 가능한 구조를 먼저 추가하고 사용 전환이 끝난 뒤 오래된 구조를 제거한다.
MariaDB 10.11의 폐기 가능한 테스트 DB에서 name을 display_name으로 전환한다. 실제 DDL의 잠금과 소요 시간은 테이블 크기와 정의에 따라 달라지므로 별도 검증이 필요하다.
변경을 세 단계로 나누는 이유
각 중간 상태에서도 배포 가능한 스키마를 유지해야 한다. 애플리케이션 롤백 가능 기간에는 구버전이 기대하는 기존 컬럼을 보존한다.
호환성을 유지하며 세 단계로 전환하기
1단계 Expand
ALTER TABLE users
ADD COLUMN display_name VARCHAR(100) NULL;
SHOW CREATE TABLE users;
처음부터 NOT NULL을 강제하지 않고 호환 가능한 nullable 컬럼을 추가한다. 실행 전 테스트 복제본에서 DDL 방식, 예상 잠금과 디스크 여유를 확인한다.
신버전은 전환 기간에 두 컬럼을 함께 쓴다. 이중 쓰기는 영구 설계가 아니며 둘 중 하나만 실패했을 때 트랜잭션을 어떻게 처리할지 정해야 한다.

2단계 Backfill과 읽기 전환
UPDATE users
SET display_name = name
WHERE display_name IS NULL
ORDER BY user_id
LIMIT 100;
SELECT COUNT(*) AS remaining
FROM users
WHERE display_name IS NULL;
대량 UPDATE 한 번 대신 PK 범위를 기준으로 작은 배치를 반복한다. 실행 시간, 영향 행 수, 복제 지연과 오류를 기록하고 중단할 수 있어야 한다.
NULL이 0이 된 뒤 신버전 읽기를 신규 컬럼으로 전환한다. 이 기간에도 구버전 롤백 테스트와 신버전 결과를 함께 확인한다.
3단계 Contract
SELECT COUNT(*) AS remaining
FROM users
WHERE display_name IS NULL;
ALTER TABLE users
MODIFY display_name VARCHAR(100) NOT NULL;
기존 컬럼 제거는 별도 변경으로 미룬다. 배포 기록, 로그와 쿼리에서 기존 사용이 사라지고 롤백 정책이 새 구조를 기준으로 바뀐 뒤에만 수행한다.

단계별 통과 조건
| 단계 | 통과 조건 | 되돌리기 |
|---|---|---|
| Expand | 구버전 읽기·쓰기 정상 | 신규 컬럼 미사용 유지 |
| Backfill | 오류 0, NULL 감소 | 배치 중단 |
| 읽기 전환 | 구·신 결과 일치 | 기존 읽기로 복귀 |
| Contract | 기존 사용처 0 | 백업과 재배포 필요 |
전환 과정에서 막히는 지점
이중 쓰기 값이 달라진다
증상
두 컬럼이 일부 행에서 불일치한다.
원인
한 쓰기 경로가 신규 컬럼 갱신을 누락했거나 트랜잭션 경계가 다르다.
확인 방법
SELECT user_id, name, display_name
FROM users
WHERE NOT (name <=> display_name)
LIMIT 100;
해결
누락 경로를 찾고 안전한 범위만 다시 backfill한다.
예방
전환 기간의 불일치 개수를 배포 게이트로 둔다.
컬럼 제거 후 롤백이 실패한다
증상
구버전이 존재하지 않는 컬럼을 조회한다.
원인
롤백 가능 기간 전에 Contract를 수행했다.
확인 방법
이전 릴리스의 SQL과 ORM 매핑을 검사한다.
해결
호환 릴리스를 먼저 배포하거나 백업에서 복구한다.
예방
파괴적 DDL은 코드 전환과 다른 배포 창에서 승인한다.
Contract 승인 체크리스트
- 구버전과 신버전이 Expand 구조에서 모두 동작하는가?
- Backfill 오류와 신규 컬럼 NULL이 0건인가?
- 기존 컬럼 참조가 제거됐는가?
- 롤백 기간이 종료되고 복구 절차를 확인했는가?
참고 자료
한 줄 요약
Expand-Contract는 호환 컬럼 추가, 데이터·읽기 전환, 기존 구조 제거를 분리하고 각 단계의 검증과 롤백 경계를 유지하는 방식이다.
댓글 0