왜 DB 마이그레이션이 문제인가

애플리케이션 코드는 블루-그린 배포로 무중단 전환이 쉽지만, DB는 상태(state)를 가지기 때문에 같은 방식이 통하지 않는다. 스키마를 바꾸는 순간 구버전 코드와 신버전 코드가 동시에 같은 테이블을 바라보게 되고, NOT NULL 컬럼 추가나 컬럼 삭제 같은 파괴적 변경은 한쪽 버전을 즉시 죽인다. 또한 대용량 테이블의 ALTER는 락(lock)을 잡아 쓰기를 멈추게 한다. 무중단의 핵심은 "구버전과 신버전이 같은 스키마 위에서 동시에 정상 동작하는 구간"을 만드는 것이다.

확장 후 축소(Expand-Contract) 패턴

단일 원자적 변경 대신 여러 단계로 나눈다. 각 단계는 이전 코드와 항상 호환된다.

  • Expand: 새 컬럼/테이블을 추가하되 nullable로. 기존 코드는 영향받지 않는다.
  • Migrate: 애플리케이션이 신·구 컬럼에 동시 기록(dual-write)하고, 과거 데이터를 백필한다.
  • Contract: 모든 인스턴스가 신버전으로 교체된 뒤, 구 컬럼을 삭제한다.

단계별 안전성 비교

변경 종류즉시 실행Expand-Contract
컬럼 추가(nullable)안전불필요
컬럼 이름 변경구버전 즉시 장애추가→dual-write→백필→삭제
NOT NULL 추가기존 NULL 행에서 실패백필 후 제약 추가
컬럼 삭제구버전 SELECT 장애배포 완료 후 삭제

컬럼 이름 변경 실전 예시

user_name을 full_name으로 바꾸는 경우를 SQL로 단계화한다. 한 번의 마이그레이션에 하나의 안전한 변경만 담는 것이 원칙이다.

-- 1단계 (Expand): 새 컬럼 추가, 기존 코드 무영향
ALTER TABLE users ADD COLUMN full_name varchar(255);

-- 2단계 (Backfill): 배치로 나눠 락 회피
UPDATE users SET full_name = user_name
WHERE full_name IS NULL AND id BETWEEN 1 AND 10000;

-- 3단계 (Contract): 신버전 배포 완료 후 별도 마이그레이션에서
ALTER TABLE users DROP COLUMN user_name;

애플리케이션의 이중 기록

Expand와 Contract 사이 구간에서는 애플리케이션이 두 컬럼을 모두 채워야, 구버전이 아직 읽고 있어도 데이터가 일관된다. 신버전 코드는 다음처럼 쓰기 시점에 양쪽을 갱신한다.

def save_user(conn, user_id, name):
    # 전환 구간: 신·구 컬럼 동시 기록
    conn.execute(
        """
        UPDATE users
        SET full_name = %(name)s,
            user_name = %(name)s   -- Contract 이후 제거
        WHERE id = %(id)s
        """,
        {"name": name, "id": user_id},
    )

온라인 스키마 변경 도구

수백만 행 테이블에서 ALTER는 장시간 락을 유발한다. MySQL은 gh-ost나 pt-online-schema-change가 그림자 테이블을 만들어 트리거로 변경분을 복제한 뒤 원자적으로 교체한다. PostgreSQL은 대부분의 ADD COLUMN이 이미 논블로킹이며, 인덱스는 CREATE INDEX CONCURRENTLY를 쓴다.

# gh-ost로 무중단 컬럼 추가
gh-ost \
  --host=db-primary --database=app --table=users \
  --alter="ADD COLUMN full_name varchar(255)" \
  --max-load="Threads_running=25" \
  --chunk-size=1000 \
  --cut-over=default \
  --execute

롤백과 검증

Expand-Contract의 장점은 각 단계가 되돌리기 쉽다는 점이다. 새 컬럼만 추가한 상태라면 롤백은 이전 코드 재배포로 끝난다. 반대로 Contract(삭제)는 되돌릴 수 없으므로, 반드시 모든 인스턴스 전환과 관측(에러율·지연) 안정화를 확인한 뒤 실행한다. 백필은 반드시 배치로 나눠 복제 지연(replication lag)을 모니터링하고, 임계치를 넘으면 자동으로 쉬어야 한다.

주의점

  • 한 배포에 파괴적 변경과 코드 변경을 섞지 말 것. 스키마 변경과 그것을 사용하는 코드는 별도 배포로 분리한다.
  • dual-write 구간을 잊고 방치하면 기술 부채가 된다. Contract 단계에 만료일을 정해 티켓으로 추적한다.
  • 외래 키와 유니크 제약은 백필 완료 전에는 추가하지 않는다. 중간 상태의 데이터가 제약을 위반할 수 있다.
  • 마이그레이션은 항상 스테이징에서 실제 데이터 규모에 가깝게 리허설한 뒤 프로덕션에 적용한다.