개인정보처리방침© 2026 DEV BAK - 기술블로그. All rights reserved.
DEV BAK - 기술블로그
포스트 검색
Backend

무중단 PostgreSQL 스키마 마이그레이션 실전 가이드 — Expand-Contract 패턴으로 컬럼 리네임부터 타입 변경까지 롤백 없이 배포하기

sql
END;
$$;
 
CALL backfill_preferences();
sql
-- 3. NOT VALID로 제약을 빠르게 추가 (신규 행만 즉시 검증)
SET lock_timeout = '2s';
ALTER TABLE users
  ADD CONSTRAINT users_preferences_not_null
  CHECK (preferences IS NOT NULL) NOT VALID;
 
-- 4. 기존 행 검증 (SHARE UPDATE EXCLUSIVE 락 — SELECT·대부분의 DML 차단 없음)
SET lock_timeout = '2s';
ALTER TABLE users VALIDATE CONSTRAINT users_preferences_not_null;
 
-- 5. PG 12+: 검증된 CHECK를 재활용해 실제 NOT NULL로 전환 (풀스캔 없음)
SET lock_timeout = '2s';
ALTER TABLE users ALTER COLUMN preferences SET NOT NULL;
 
-- 6. NOT NULL이 보장되므로 CHECK 제약 제거 (선택)
SET lock_timeout = '2s';
ALTER TABLE users DROP CONSTRAINT users_preferences_not_null;

NOT VALID + VALIDATE CONSTRAINT 조합은 PostgreSQL 12+에서 특히 강력합니다. VALIDATE CONSTRAINT는 SHARE UPDATE EXCLUSIVE 락만 잡기 때문에 SELECT는 물론이고 대부분의 DML도 막지 않습니다. 그리고 PostgreSQL 12+에서는 검증된 CHECK 제약이 존재하면 5단계의 SET NOT NULL이 풀스캔 없이 처리됩니다.


시나리오 4: 인덱스 추가

CREATE INDEX는 테이블 락을 잡지만, CREATE INDEX CONCURRENTLY는 백그라운드에서 실행됩니다.

sql
-- 잘못된 방법: 테이블 락 발생
CREATE INDEX idx_users_email ON users(email);
 
-- 올바른 방법: 트랜잭션 블록 밖에서 실행
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

주의할 점이 두 가지 있습니다. 첫째, CONCURRENTLY는 트랜잭션 블록 안에서 실행할 수 없습니다. 둘째, 실패하면 INVALID 상태의 인덱스가 남으니 확인 후 정리가 필요합니다.

sql
-- INVALID 인덱스 확인
SELECT indexrelid::regclass, indisvalid
FROM pg_index
WHERE indexrelid = 'idx_users_email'::regclass;
 
-- INVALID면 먼저 드롭 후 재생성
DROP INDEX CONCURRENTLY IF EXISTS idx_users_email;
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);

어떤 전략을 써야 할까? 의사결정 흐름

다이어그램 1

"대형 테이블"의 기준은 절대적이지 않습니다. 수십만 건까지는 lock_timeout을 걸고 직접 ALTER COLUMN TYPE을 시도해볼 수 있지만, 수백만 건이 넘는다면 Expand-Contract 전체 절차를 밟는 것이 좋습니다. 테이블 크기 외에도 쓰기 빈도, 트랜잭션 길이, 동시성이 판단 기준에 들어갑니다.


장단점 분석

장점

항목 설명
무중단 배포 각 단계가 독립적으로 안전해서 서비스 가용성이 유지됩니다
자연스러운 복귀 Contract 이전에는 구 구조가 살아있어, 이전 코드로 되돌아가는 것만으로 복구됩니다
점진적 검증 단계별로 데이터 정합성을 확인할 수 있어 신뢰도가 높아집니다
피처 플래그 통합 코드 배포와 데이터 컷오버를 완전히 분리할 수 있습니다

단점 및 주의사항

항목 설명
운영 복잡도 증가 단순 DDL 1회 대신 3~4회의 배포가 필요합니다
듀얼 라이트 오버헤드 두 컬럼에 동시에 쓰는 기간 동안 스토리지와 I/O가 증가합니다
트리거 성능 영향 동기화 트리거가 쓰기 성능에 영향을 줍니다. 고부하 테이블에서는 사전 벤치마킹이 필요합니다
배치 백필 소요 시간 수억 건 테이블이라면 백필만 며칠이 걸릴 수 있어 Contract 일정에 충분한 여유를 반영하는 것이 좋습니다

실무에서 자주 하는 실수

고아 트리거: Contract 단계에서 트리거 제거를 빠뜨리고 컬럼을 먼저 드롭하면, 삭제된 컬럼을 참조하는 트리거가 살아있어 모든 INSERT/UPDATE에서 에러가 납니다. 체크리스트나 마이그레이션 스크립트에서 반드시 트리거와 함수를 컬럼보다 먼저 삭제하는 순서를 지키는 것이 좋습니다.

트리거 오버라이트: 듀얼 라이트 없이 신 컬럼에만 쓰면서 트리거를 남겨두면, 트리거가 구 컬럼의 이전 값으로 신 컬럼을 덮어씁니다. 트리거는 Contract 직전까지 살아있어야 하고, 그 동안은 반드시 두 컬럼 모두에 쓰는 방식을 유지해야 합니다.

스테이징 환경 데이터 규모: 개발 DB에서 2초 걸리는 마이그레이션이 프로덕션에서 2시간 걸릴 수 있습니다. 프로덕션과 유사한 규모의 스테이징 환경에서 반드시 검증하는 것이 좋습니다.

배치 크기 결정: 너무 크면 각 배치가 락을 오래 잡고, 너무 작으면 전체 시간이 길어집니다. 일반적으로 1만~10만 건 범위에서 시작해서 조정하는 방식이 무난합니다.

ORM과의 통합: ORM을 쓴다면 Expand 단계에서 모델이 두 컬럼을 모두 인식해야 할 수 있습니다. 프레임워크마다 처리 방법이 다르니 사전에 확인하는 것이 좋습니다.


도구 비교

도구 역할 특징
pgroll (Xata) Expand-Contract 자동화 버전드 뷰로 구·신 스키마 동시 서비스, PostgreSQL 14+, RDS/Aurora 지원
squawk 마이그레이션 린터 lock_timeout 누락·비 CONCURRENTLY 인덱스 생성 등 안전하지 않은 DDL 패턴을 PR 단계에서 자동 차단
pg_repack 온라인 테이블 재팩킹 ACCESS EXCLUSIVE 없이 물리 구조 재구성 (DDL 도구가 아님)
Bytebase 스키마 변경 관리 플랫폼 GUI 기반 리뷰·승인 워크플로우

pgroll은 Expand-Contract 패턴을 자동화하면서 마이그레이션 윈도우 동안 구·신 스키마를 버전드 뷰로 동시에 서비스합니다. 복잡한 마이그레이션이 많은 팀이라면 검토해볼 만합니다.


마치며

스키마 변경은 작은 실수 하나가 큰 장애로 이어질 수 있는 영역입니다. 그렇다고 매번 유지보수 윈도우를 잡아야 할 만큼 어렵지도 않습니다. Expand-Contract 패턴을 팀에 정착시키면 스키마 변경을 다른 기능 배포와 같은 방식으로 처리할 수 있게 됩니다.

핵심을 세 가지로 정리하면:

첫째, 모든 DDL 앞에는 SET lock_timeout을 설정하는 것이 좋습니다. 이것만으로도 락 큐 연쇄 장애의 상당 부분을 방지할 수 있습니다.

둘째, 타입 변경은 Full Table Rewrite를 피하는 것이 핵심입니다. 새 컬럼 추가 → 트리거 동기화 → 배치 백필 → 코드 전환(듀얼 라이트 유지) → 트리거 삭제 → 구 컬럼 삭제 순서를 따르면 됩니다.

셋째, 배치 백필은 항상 범위 기반으로, PROCEDURE를 활용해 독립 트랜잭션으로 처리하는 것이 좋습니다. 단일 UPDATE로 수백만 건을 한 번에 바꾸는 것은 DDL 락 못지않은 장애를 만들 수 있습니다.

지금 바로 시작하는 세 단계를 제안한다면:

  1. squawk를 CI에 통합해서 안전하지 않은 DDL 패턴을 PR 단계에서 차단하는 것이 가장 빠른 첫 걸음입니다.
  2. 기존 마이그레이션 스크립트를 검토해서 lock_timeout 없이 실행되는 DDL을 찾아 수정해보시면 좋습니다.
  3. 다음 스키마 변경에 Expand-Contract를 적용해서 단계별 배포를 직접 경험해보시면, 이후엔 자연스럽게 이 방식으로 접근하게 됩니다.

처음엔 배포 횟수가 늘어나서 번거롭게 느껴질 수 있습니다. 하지만 배포 복잡도가 올라간 만큼 서비스 가용성과 팀의 자신감이 함께 올라갑니다.


참고 자료

  • Zero-Downtime PostgreSQL Schema Migrations: Expand/Contract vs Blue-Green Deployment — DEV Community
  • Database Migrations in Production: Zero-Downtime Schema Changes (2026 Guide) — DEV Community
  • Database Migrations Without Downtime — Expand-Contract, Shadow Tables, and Feature Flags | datasops Blog
  • Zero-Downtime PostgreSQL Migrations: Expand/Contract, Backfill and Rollback Strategies | Michal Drozd
  • Using the expand and contract pattern | Prisma's Data Guide
  • Zero-downtime Postgres schema migrations need this: lock_timeout and retries | PostgresAI
  • Schema changes and the Postgres lock queue — Xata Blog
  • pgroll — Zero-downtime, reversible, schema changes for PostgreSQL
  • Schema changes and the power of expand-contract with pgroll — Xata Blog
  • How to perform Postgres schema changes in production with zero downtime — Xata Blog
  • Applying migrations safely | Squawk — a linter for Postgres migrations
  • Top Open Source Postgres Migration Tools in 2026 | Bytebase
  • Database Migrations Without Drama: Expand/Contract in Practice
  • Database Migration Strategies for Zero-Downtime Deployments | DeployHQ
  • Change management tools and techniques — PostgreSQL Wiki
#PostgreSQL#Schema Migration#Expand-Contract#무중단 배포#DDL#CREATE INDEX CONCURRENTLY
공유하기

목차

시나리오 4: 인덱스 추가어떤 전략을 써야 할까? 의사결정 흐름장단점 분석장점단점 및 주의사항실무에서 자주 하는 실수도구 비교마치며참고 자료

추천 포스트

공개키만 남기고 비밀번호를 걷어내기: Node.js와 SimpleWebAuthn으로 패스키 등록·인증을 구현하고 기존 로그인과 단계적으로 통합하기
Backend

공개키만 남기고 비밀번호를 걷어내기: Node.js와 SimpleWebAuthn으로 패스키 등록·인증을 구현하고 기존 로그인과 단계적으로 통합하기

인증 시스템을 처음부터 다시 짜야 한다는 얘기를 들으면 솔직히 긴장됩니다. 비밀번호 해싱, 세션 관리, 2FA 연동까지 이미 쌓아온 코드가 있는데, 거기에 WebAuthn이라는 새로운 개념을 얹어야 한다니. 저도 처음에 W3C 명세를 펼쳤다가 CBOR, COSE, at…

2026년 07월 19일읽는 데 27분
Node.js 22 Permission Model 실전 가이드 — `--allow-fs-read`로 프로세스 권한을 최소화하고 공급망 공격 표면을 줄이는 법
Backend

Node.js 22 Permission Model 실전 가이드 — `--allow-fs-read`로 프로세스 권한을 최소화하고 공급망 공격 표면을 줄이는 법

npm 생태계에서 공급망 공격은 꾸준히 늘고 있다. 패키지 하나가 침해되는 순간, npm install 로 당겨온 수백 개의 의존성 중 어느 것도 온전히 신뢰하기 어려워진다. 락파일 관리, 의존성 감사, npm audit 은 여전히 유효하지만, 침해가 발생한 이후를 위…

2026년 07월 20일읽는 데 14분
API 스키마 한 벌로 OpenAPI 3.1·gRPC Protobuf·TypeScript 클라이언트를 동시에 뽑아내기 — TypeSpec 1.0 실전 가이드
Backend

API 스키마 한 벌로 OpenAPI 3.1·gRPC Protobuf·TypeScript 클라이언트를 동시에 뽑아내기 — TypeSpec 1.0 실전 가이드

작년에 직접 겪은 일입니다. 결제팀이 /payments/{id} 응답에서 settled at 필드를 completedAt 으로 이름을 바꿨습니다. JIRA 티켓도 있었고, 코드 리뷰도 통과했고, 사내 Slack에 공지도 됐습니다. 그런데 주문팀의 BFF는 여전히 set…

2026년 07월 20일읽는 데 23분
`pg`, `ioredis`, `@aws-sdk/client-s3`를 지워도 되는 이유 — Bun 네이티브 클라이언트로 스토리지 드라이버 의존성 줄이기
Backend

`pg`, `ioredis`, `@aws-sdk/client-s3`를 지워도 되는 이유 — Bun 네이티브 클라이언트로 스토리지 드라이버 의존성 줄이기

새 프로젝트를 시작할 때마다 npm install pg ioredis @aws sdk/client s3 를 치고, @types/ 패키지를 맞추고, 버전 충돌을 잡다가 정작 코드는 한 줄도 못 쓰고 오후가 지나가는 경험 — 낯설지 않으실 겁니다. Bun 1.2가 SQL·…

2026년 07월 19일읽는 데 20분
Fastify v5 + TypeBox로 TypeScript REST API 구축하기
Backend

Fastify v5 + TypeBox로 TypeScript REST API 구축하기

Express로 API를 작성할 때 생기는 구조적 문제를 코드로 먼저 보겠습니다. TypeScript 타입, 런타임 검증 규칙, API 문서가 각각 다른 곳에 있습니다. 셋 중 하나가 바뀌어도 나머지가 자동으로 따라오지 않습니다. Fastify v5 + TypeBox는…

2026년 07월 19일읽는 데 15분
Node.js 비동기 작업을 안정적으로 처리하는 BullMQ 5 잡 큐 — 재시도 전략·Dead Letter Queue·Sandboxed Processor·Prometheus 메트릭 연동까지
Backend

Node.js 비동기 작업을 안정적으로 처리하는 BullMQ 5 잡 큐 — 재시도 전략·Dead Letter Queue·Sandboxed Processor·Prometheus 메트릭 연동까지

회원 가입 API를 만들다 보면 어느 순간 이런 생각이 듭니다. "환영 이메일을 발송하는 동안 HTTP 응답이 블로킹되는 게 맞는 걸까?" 저도 처음엔 그냥 await sendEmail() 을 응답 전에 넣었는데, SendGrid 타임아웃이 터지는 순간 모든 가입 요청…

2026년 07월 19일읽는 데 23분