이 글의 목차
“죄송합니다, 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):
username과email두 컬럼 모두에 데이터를 저장합니다. - 데이터 읽기(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:
CHECK (column IS NOT NULL) NOT VALID제약조건을 먼저 추가합니다. (NOT VALID옵션은 기존 데이터 검사를 건너뜁니다.)ALTER TABLE ... VALIDATE CONSTRAINT명령어로 백그라운드에서 천천히 데이터를 검증합니다.- 검증이 완료되면 정식
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은 마법이 아닙니다. 번거롭고 귀찮은 과정을 묵묵히 수행하며 안전을 최우선으로 생각하는 엔지니어의 노력에서 만들어집니다.