본문으로 건너뛰기
Zero Downtime 데이터베이스 마이그레이션 가이드

# Zero Downtime 데이터베이스 마이그레이션: 점검 공지 없이 스키마 변경하기

Table of Contents

“죄송합니다, DB 작업 때문에 10분만 서비스 점검합니다.”

2025년 현재, 이런 공지를 띄우는 서비스는 경쟁력을 잃기 쉽습니다. 사용자들은 새벽 3시에도 쇼핑을 즐기고, 글로벌 서비스라면 시차 없는 트래픽이 24시간 쏟아집니다. 365일 24시간 무중단 서비스가 표준이 된 지금, 데이터베이스 스키마 변경은 백엔드 엔지니어에게 가장 부담스러운 작업 중 하나입니다.

ALTER TABLE 쿼리 하나를 잘못 실행했다가 수천만 건의 데이터가 있는 테이블에 락(Lock)이 걸린다면 어떻게 될까요? 모든 API 요청은 타임아웃 되고, 에러 알람이 빗발치며, 결국 장애 보고서를 작성해야 하는 상황이 발생할 수 있습니다.

이 글에서는 서비스를 중단하지 않고(Zero Downtime) 안전하게 데이터베이스 스키마를 변경하는 전략, 그중에서도 Expand-Contract 패턴을 중심으로 한 실전 기법들을 다룹니다.

스키마 변경이 위험한 이유

개발 환경에서는 문제없던 쿼리가 프로덕션 환경에서는 치명적인 장애를 일으킬 수 있습니다.

1. 테이블 락 (Table Lock)에 의한 서비스 중단

대부분의 관계형 데이터베이스(RDBMS)는 구조를 변경할 때 데이터 무결성을 위해 테이블 잠금(Lock)을 겁니다.

ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending';

MySQL의 구버전이나 PostgreSQL의 특정 상황에서 위와 같은 쿼리는 테이블 전체를 다시 쓰는(Rewrite) 작업을 유발할 수 있습니다. 레코드가 1억 개라면 작업이 완료될 때까지 몇 시간 동안 주문 테이블은 ‘읽기 전용’ 상태가 되거나 아예 접근이 불가능해져 서비스가 마비됩니다.

2. 애플리케이션 배포 간의 호환성 문제

무중단 배포(Rolling Update)를 진행하는 동안에는 **구버전 코드(v1)**와 **신버전 코드(v2)**가 공존하는 시간이 반드시 존재합니다.

  • v2 애플리케이션: 새로 추가된 new_column을 읽으려 시도합니다.
  • DB 상태: 아직 마이그레이션이 완료되지 않았다면 new_column이 없어 에러가 발생합니다.
  • 반대의 경우: 마이그레이션을 먼저 해서 컬럼 이름을 old에서 new로 바꿨다면, 아직 남아있는 v1 애플리케이션이 old 컬럼을 찾지 못해 에러가 발생합니다.

이러한 딜레마를 해결하고 호환성을 유지하는 것이 무중단 마이그레이션의 핵심입니다.

핵심 전략: Expand-Contract 패턴

이 패턴은 변경을 한 번에 처리하려 하지 않고, **확장(Expand), 이주(Migrate), 축소(Contract)**의 3단계로 나누어 진행하는 전략입니다. 이를 ‘Parallel Change’라고도 부릅니다. 예를 들어 username 필드의 이름을 email로 변경한다고 가정해 보겠습니다.

1단계: Expand (확장)

기존 username 컬럼은 그대로 두고, 새로운 email 컬럼을 추가합니다. 이때 중요한 점은 email 컬럼을 **Nullable(NULL 허용)**로 생성해야 한다는 것입니다. (Not Null 제약조건이 있으면 기존 데이터와의 충돌로 에러가 발생합니다.)

ALTER TABLE users ADD COLUMN email VARCHAR(255);

이제 애플리케이션(v1.1)을 배포합니다. 이 버전의 코드는 **이중 쓰기(Dual Write)**를 수행해야 합니다.

  • 데이터 저장(INSERT/UPDATE): usernameemail 두 컬럼 모두에 데이터를 저장합니다.
  • 데이터 읽기(SELECT): 여전히 기존의 username을 사용합니다.

2단계: Migrate (데이터 이주)

이제 기존에 쌓여있던 username의 데이터를 email 컬럼으로 복사해야 합니다. 한 번에 UPDATE 쿼리를 실행하면 DB에 큰 부하를 줄 수 있으므로, 배치 작업을 통해 조금씩 나누어 복사합니다.

-- 부하를 줄이기 위해 조금씩 나누어 실행
UPDATE users SET email = username WHERE email IS NULL LIMIT 1000;

모든 데이터 복사가 완료되면 애플리케이션(v1.2)을 배포합니다. 이 버전부터는 email 컬럼을 주력으로 사용합니다.

  • 읽기/쓰기: 모두 email 컬럼을 사용합니다.
  • 롤백 대비: 만약을 대비해 username에도 당분간 데이터를 같이 넣어줍니다.

3단계: Contract (축소)

데이터 정합성이 확인되고 에러가 없다면, 이제 구버전의 흔적을 지울 차례입니다. 애플리케이션(v1.3)에서 username 관련 로직을 완전히 제거하여 배포합니다. 그 후 데이터베이스에서 더 이상 쓰이지 않는 컬럼을 삭제합니다.

ALTER TABLE users DROP COLUMN username;

과정이 다소 번거로워 보일 수 있습니다. 하지만 이 방법만이 서비스 중단 없이, 언제든 롤백 가능한 상태를 유지하며 스키마를 변경할 수 있는 가장 안전한 길입니다.

실전 팁: 작업별 안전한 쿼리 가이드

1. 컬럼 추가하기

PostgreSQL 11 이상이나 MySQL 8.0 이상에서는 DEFAULT 값 없이 컬럼을 추가하는 작업이 메타데이터만 변경하므로 매우 빠릅니다. 하지만 DEFAULT 값을 포함하거나 테이블 크기가 매우 크다면 주의가 필요합니다.

  • Tip: 일단 DEFAULT 없이 Nullable로 컬럼을 생성하고, 애플리케이션이나 배치 쿼리로 값을 채운 뒤, 마지막에 ALTER COLUMN SET DEFAULT를 적용하는 것이 안전합니다.

2. 인덱스 생성하기 (가장 중요)

운영 중 인덱스를 생성하다가 서버가 멈추는 경우가 빈번합니다.

  • PostgreSQL: 반드시 CONCURRENTLY 옵션을 사용해야 합니다.

    CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

    이 옵션은 락을 걸지 않고 인덱스를 생성합니다. 일반 생성보다 시간은 더 걸리지만, 서비스에는 영향을 주지 않습니다.

  • MySQL: 5.6 버전부터 Online DDL을 지원하지만, 트래픽이 많은 상황에서는 복제 지연(Replication Lag)이 발생할 수 있습니다. 대규모 테이블이라면 gh-ost 같은 외부 도구 사용을 권장합니다.

3. NOT NULL 제약조건 추가하기

수억 건의 데이터에 대해 NOT NULL 제약조건을 검사하려면 테이블 전체를 스캔(Full Scan)해야 하므로 부하가 큽니다.

  • PostgreSQL:
    1. CHECK (column IS NOT NULL) NOT VALID 제약조건을 먼저 추가합니다. (NOT VALID 옵션은 기존 데이터 검사를 건너뜁니다.)
    2. ALTER TABLE ... VALIDATE CONSTRAINT 명령어로 백그라운드에서 천천히 데이터를 검증합니다.
    3. 검증이 완료되면 정식 NOT NULL 제약조건으로 변경합니다.

대용량 테이블을 위한 도구들

테이블 사이즈가 수백 GB를 넘어 ALTER TABLE 명령 자체가 부담스럽다면, 전문 도구의 힘을 빌려야 합니다.

gh-ost (GitHub Online Schema Transitions)

GitHub에서 개발한 MySQL용 무중단 스키마 변경 도구입니다.

  • 원리: 원본 테이블을 직접 건드리지 않고, 스키마가 변경된 **복제 테이블(Ghost Table)**을 생성합니다. 기존 데이터를 백그라운드에서 조금씩 복사하고, 그동안 발생하는 실시간 변경사항(Binlog)을 지속적으로 반영합니다. 동기화가 완료되면 테이블 이름을 맞바꾸는(Atomic Cutover) 방식으로 전환합니다.
  • 장점: 원본 테이블에 락을 걸지 않으며, 작업 중 언제든 중단할 수 있고 부하 조절이 가능합니다.

pt-online-schema-change (Percona Toolkit)

전통적으로 많이 사용되어 온 도구입니다. 트리거(Trigger)를 사용하여 데이터를 동기화합니다.

  • 단점: 트리거를 사용하기 때문에 데이터베이스에 추가적인 오버헤드가 발생할 수 있습니다. 최근에는 gh-ost를 더 선호하는 추세입니다.

마치며

“DB 스키마 변경이 두렵다”는 것은 아직 충분한 준비와 검증 프로세스가 갖춰지지 않았다는 신호일 수 있습니다. 모든 변경 작업은 언제든 되돌릴 수 있어야(Reversible) 합니다.

Expand-Contract 패턴을 적용하고, 로컬이나 스테이징 환경에서 실제 운영 데이터와 유사한 규모의 더미 데이터로 충분히 연습해 보세요. 10만 건일 때 0.1초 걸리던 쿼리가 1억 건일 때는 10분이 걸릴 수도 있다는 사실을 직접 경험하고 대비해야 합니다.

Zero Downtime은 마법이 아닙니다. 번거롭고 귀찮은 과정을 묵묵히 수행하며 안전을 최우선으로 생각하는 엔지니어의 노력에서 만들어집니다.

이 글 공유하기:
My avatar

글을 마치며

이 글이 도움이 되었기를 바랍니다. 궁금한 점이나 의견이 있다면 댓글로 남겨주세요.

더 많은 기술 인사이트와 개발 경험을 공유하고 있으니, 다른 포스트도 확인해보세요.

유럽살며 여행하며 코딩하는 노마드의 여정을 함께 나누며, 함께 성장하는 개발자 커뮤니티를 만들어가요! 🚀


관련 포스트