infra
Platform

모듈 맵

[Database] Flyway와 Liquibase를 이용한 DB 마이그레이션 버전 관리

0 / 37 완료

펼치기
0 / 37 완료0%

데이터베이스 설계 & 튜닝 · 24 / 37

[Database] Flyway와 Liquibase를 이용한 DB 마이그레이션 버전 관리

데이터베이스 스키마 변경을 코드로 관리하고 안전하게 운영 환경에 적용하는 방법을 배웁니다

🚨INCIDENT ALERT
HIGH

서비스가 배포된 뒤에도 스키마는 계속 바뀝니다. 변경 이력을 코드처럼 관리하지 않으면 개발·스테이징·운영 DB가 서로 다른 상태가 됩니다. 마이그레이션 버전 관리는 팀이 안전하게 DB 구조를 진화시키는 기본 장치입니다.

이번 챕터에서 배울 것

스키마 변경을 버전 관리 시스템(Git)에 커밋된 코드로 관리하면 팀 전체가 동일한 순서로 변경사항을 적용할 수 있습니다. 이 모듈에서는 주요 마이그레이션 도구의 동작 방식과 운영 환경에서 안전하게 스키마를 변경하는 전략을 배웁니다.

  • 1마이그레이션이 필요한 이유와 수동 관리의 문제점을 설명할 수 있다
  • 2Flyway로 SQL 파일 기반 스키마 버전 관리를 구성할 수 있다
  • 3Liquibase의 플랫폼 독립적인 changeset 방식을 적용할 수 있다
  • 4Flyway와 Liquibase를 비교해 상황에 맞는 도구를 선택할 수 있다
  • 5Prisma migrate로 ORM 통합 마이그레이션을 실행할 수 있다
  • 6Expand-Contract 패턴으로 무중단 마이그레이션을 설계할 수 있다

마이그레이션 — Flyway, Liquibase, 스키마 버전 관리

운영 중인 서비스에서 데이터베이스 스키마를 변경하는 일은 가장 위험한 작업 중 하나입니다. 컬럼 하나를 잘못 삭제하거나 잠금 없이 인덱스를 추가했다가 수분간 서비스가 중단될 수 있습니다. DDL 변경은 대부분 롤백이 불가능하고, 실행 중 테이블 락이 걸리며, 구버전 애플리케이션과의 호환성 문제를 일으킵니다. 마이그레이션 도구는 이러한 변경 이력을 코드로 관리하고 재현 가능하게 만들어 줍니다.


💡개념

마이그레이션 도구 — Flyway와 Liquibase 비교

새 개발자가 합류해서 로컬 개발환경을 세팅합니다. git clone까지는 잘 됐는데 앱이 실행되지 않습니다. 테이블 컬럼이 맞지 않는다는 에러입니다. 시니어한테 물어보니 "지난주에 컬럼 추가했어요, 직접 ALTER하세요"라고 합니다. 문서도 없고, 언제 무엇이 바뀌었는지도 모릅니다. 마이그레이션 도구 없이 스키마를 관리하면 팀 규모가 커질수록 이런 혼란이 반복됩니다.

마이그레이션 도구 — Flyway와 Liquibase 비교확대

마이그레이션 없이 스키마를 관리하면 생기는 일

팀에서 마이그레이션 도구 없이 스키마를 변경하면 다음과 같은 문제가 발생합니다.

  • 환경 불일치: 개발자 A는 컬럼을 추가했지만 개발자 B는 모름
  • 재현 불가: 새 개발자가 합류했을 때 정확한 스키마 상태를 알 수 없음
  • 배포 사고: 운영 DB에 변경사항 적용 순서가 잘못되어 데이터 손상
  • 롤백 불가: 어떤 변경이 언제 이루어졌는지 추적이 어려움

Flyway vs Liquibase 비교

두 도구 모두 마이그레이션 이력을 DB 테이블에 저장하고 체크섬으로 변경 여부를 감지합니다. 선택 기준은 팀의 SQL 친숙도와 멀티 DBMS 지원 필요 여부입니다.

항목FlywayLiquibase
파일 형식주로 SQLXML, YAML, JSON, SQL
학습 곡선낮음중간
롤백수동 SQL 작성changeset에 rollback 내장 가능
DBMS 추상화낮음높음 (플랫폼 독립)
체크섬 검증강제선택적
이력 테이블flyway_schema_historyDATABASECHANGELOG
기업 지원유료 플랜 있음유료 플랜 있음
적합한 환경SQL 중심, 단순한 팀멀티 DBMS, 복잡한 변경 관리

Flyway — SQL 파일 기반 버전 관리

Flyway는 가장 단순하고 직관적인 마이그레이션 도구입니다. SQL 파일에 버전 번호를 매겨 관리하며, flyway_schema_history 테이블에 실행 이력과 체크섬을 기록합니다.

파일 명명 규칙은 V{버전}__{설명}.sql 형식이며, R__ 접두사 파일은 내용이 변경될 때마다 재실행됩니다(뷰, 함수 재정의에 유용).

migrations/ 디렉토리 예시입니다.

  • V1__create_users_table.sql
  • V2__add_email_to_users.sql
  • V3__create_orders_table.sql
  • V3.1__add_order_status_index.sql
  • R__create_user_summary_view.sql (반복 실행)
SQL
CREATE TABLE users (
    id          BIGSERIAL PRIMARY KEY,
    name        VARCHAR(100) NOT NULL,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
OUTPUT
실행 완료 또는 조회 결과가 표시됩니다.
🔍실행 후 확인할 것
  • 먼저 flyway_schema_history에서 installed_rank 순서를 확인합니다. 버전 번호가 V1 → V2 → V3 순서대로 기록돼 있지 않다면 실행 순서가 어긋난 것입니다.
  • success 컬럼 값을 확인합니다. false인 행이 있으면 해당 마이그레이션이 실패한 채 dirty 상태로 남아 있습니다 — 다음 flyway migrate 실행 시 전체가 중단됩니다.
  • checksum 컬럼을 기억해 두세요. 같은 버전의 파일을 수정하면 다음 실행 시 체크섬 불일치 오류가 납니다 — checksum이 변경됐다면 파일이 이미 적용된 후 수정된 것입니다.
위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE users ADD COLUMN email VARCHAR(255);
ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email);
SQL
CREATE TABLE orders (
    id           BIGSERIAL PRIMARY KEY,
    user_id      BIGINT NOT NULL REFERENCES users(id),
    total_amount NUMERIC(12, 2) NOT NULL,
    status       VARCHAR(20) NOT NULL DEFAULT 'pending',
    created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_orders_user_id ON orders(user_id);

Flyway가 flyway_schema_history 테이블에 기록한 이력을 조회하면 다음과 같습니다. 체크섬 컬럼을 통해 이미 적용된 파일이 수정되었는지 감지합니다.

SQL
SELECT version, description, checksum, success, installed_on
FROM flyway_schema_history
ORDER BY installed_rank;

한 번 적용된 마이그레이션 파일을 수정하면 Flyway는 체크섬 불일치를 감지하고 실행을 즉시 중단합니다. 이미 적용된 파일은 절대 수정하지 않고 새 버전 파일을 추가해야 합니다.

Spring Boot 통합은 application.yml에 다음을 추가하면 애플리케이션 시작 시 자동으로 마이그레이션을 적용합니다.

YAML
spring:
  flyway:
    enabled: true
    locations: classpath:db/migration
    baseline-on-migrate: true
    validate-on-migrate: true
XML
<dependency>
    <groupId>org.flywaydb</groupId>
    <artifactId>flyway-core</artifactId>
</dependency>

팀원이 이미 운영 DB에 적용된 V3__create_orders_table.sql을 로컬에서 수정하면, 다음 배포 시 Flyway가 체크섬 불일치를 감지하고 전체 마이그레이션 실행을 중단합니다. 운영 서버에서 마이그레이션이 실패하면 애플리케이션 시작도 실패합니다.

해결: 이미 적용된 마이그레이션 파일은 절대 수정하지 않습니다. 변경이 필요하면 새 버전 파일(V4__)을 추가합니다. Git 브랜치 보호 규칙에 "기존 마이그레이션 파일 수정 금지" 규칙을 추가하고, PR 리뷰 체크리스트에 포함시키는 것이 좋습니다.

Liquibase — XML/YAML 기반 changeset 방식

Liquibase는 XML, YAML, JSON, SQL 형식으로 변경사항을 표현할 수 있습니다. 각 changeset에 rollback 블록을 함께 정의할 수 있어, Flyway 대비 롤백 전략을 구조적으로 관리할 수 있습니다.

YAML
databaseChangeLog:
  - changeSet:
      id: 1
      author: alice
      changes:
        - createTable:
            tableName: users
            columns:
              - column:
                  name: id
                  type: BIGINT
                  autoIncrement: true
                  constraints:
                    primaryKey: true
              - column:
                  name: name
                  type: VARCHAR(100)
                  constraints:
                    nullable: false
              - column:
                  name: created_at
                  type: TIMESTAMP WITH TIME ZONE
                  defaultValueComputed: NOW()
                  constraints:
                    nullable: false

  - changeSet:
      id: 2
      author: bob
      changes:
        - addColumn:
            tableName: users
            columns:
              - column:
                  name: email
                  type: VARCHAR(255)
        - addUniqueConstraint:
            tableName: users
            columnNames: email
            constraintName: users_email_unique
      rollback:
        - dropColumn:
            tableName: users
            columnName: email

Prisma Migrate

Prisma는 ORM과 마이그레이션이 통합된 방식을 제공합니다. prisma migrate dev로 개발 환경에서 마이그레이션 파일을 생성하고, prisma migrate deploy로 운영 환경에 적용합니다.

로컬 터미널
npx prisma migrate dev --name add_email_to_users
npx prisma migrate deploy
npx prisma migrate status

위 명령으로 생성된 마이그레이션 파일의 내용은 다음과 같습니다.

위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE "users" ADD COLUMN "email" TEXT;
ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE ("email");
💡개념

flyway migrate 한 번이 실제로 하는 일 — 이력 대조부터 기록까지 5단계

flyway migrate(또는 liquibase update) 한 번이면 팀 전원의 DB가 같은 스키마로 맞춰집니다. 그런데 도구는 "이 DB에 무엇이 이미 적용됐고 무엇을 더 실행해야 하는지"를 어떻게 알까요? 비밀은 도구가 DB 안에 만들어 두는 적용 이력 테이블과, 매 실행마다 그 이력과 파일을 대조하는 5단계 절차에 있습니다. 이 흐름을 알면 체크섬 오류·부분 적용·버전 충돌이 어느 단계에서 나는지 바로 짚을 수 있습니다.

TEXT
[CI / 앱 시작]  flyway migrate
   │
   ① 버전 파일 스캔·정렬   (migrations/ 의 V1__, V2__, V3__ … 을 버전 번호 순으로 나열)
   │
   ② 적용 이력 테이블 조회  (flyway_schema_history 에서 '어디까지 적용됐는지' + 각 파일 체크섬 읽기)
   │
   ③ 미적용분만 순차 실행   (이력에 없는 버전만, 낮은 버전 → 높은 버전 순으로 DDL 실행)
   │                        → 각 파일을 트랜잭션으로 감싸 실행(DDL 트랜잭션 지원 DB)
   │
   ④ 이력 기록             (성공한 파일마다 version · checksum · success=true 를 이력에 추가)
   │
   ⑤ 실패 시 중단          (한 파일이 실패하면 그 지점에서 멈춤 → 이후 버전 미적용, 실패 행은 dirty)
   ▼
[결과]  개발·스테이징·운영이 같은 순서로 같은 스키마에 수렴

각 단계에서 무슨 일이 일어나고, 어디서 어긋나나:

단계하는 일여기서 어긋나면(증상)
① 파일 스캔·정렬migrations/의 버전 파일을 버전 번호 순으로 나열두 사람이 같은 번호(V5__)를 각자 만들면 순서 충돌·중복 버전 → 머지 때 번호 재조정 필요
② 이력 대조이력 테이블에서 적용된 버전과 각 파일 체크섬을 읽음이미 적용된 파일을 나중에 수정 → 체크섬 불일치로 실행 거부(적용분은 수정 금지, 새 파일 추가)
③ 미적용분 순차 실행이력에 없는 버전만 낮은→높은 순으로 DDL 실행V2가 만든 컬럼을 V4가 참조하는데 순서가 깨지면 실패 → 마이그레이션은 앞 버전에 의존
④ 이력 기록성공한 파일의 version·checksum·success를 기록이 기록이 있어야 다음 실행이 "미적용분만" 이어서 적용 — 환경 간 스키마 일치의 근거
⑤ 실패 시 중단실패 지점에서 멈추고 실패 행을 dirty로 남김중간 파일이 실패하면 부분 적용 상태 → dirty 정리(repair) 후 재실행. Flyway는 down 자동 롤백이 없어 되돌리기 SQL을 직접 준비

즉 마이그레이션 도구의 핵심은 ②이력 대조와 ④이력 기록입니다 — "어디까지 적용됐는지"를 DB가 스스로 기억하기에 개발·스테이징·운영이 같은 순서로 수렴합니다. 그래서 사고는 대개 코드가 아니라 규율에서 납니다: 적용된 파일 수정(②체크섬), 팀 간 버전 번호 중복(①), down을 안 만들어 되돌릴 수 없는 상태(⑤). 문제가 생기면 flyway_schema_history의 success 컬럼과 체크섬부터 보면 대부분 원인이 잡힙니다.

💡개념

무중단 마이그레이션 전략 — 컬럼 추가/삭제를 안전하게

배포 시간이 새벽 2시입니다. 컬럼 이름을 변경하는 마이그레이션을 실행했는데 5분이 지나도 끝나지 않습니다. 그 사이 서비스 API가 타임아웃으로 응답하지 않습니다. 수백만 건의 대용량 테이블에서 ALTER TABLE이 잠금을 잡고 있었던 겁니다. 결국 마이그레이션을 강제 중단했고 데이터 정합성이 깨졌습니다. DDL 변경은 올바른 순서와 패턴 없이 운영 환경에 적용하면 장애로 이어집니다.

무중단 마이그레이션 전략 — 컬럼 추가/삭제를 안전하게확대

DDL 변경의 위험성

운영 환경에서 DDL(CREATE, ALTER, DROP) 변경은 세 가지 위험을 가집니다. 첫째, ALTER TABLE은 대용량 테이블에서 수분간 테이블 잠금을 유발합니다. 둘째, 컬럼 삭제나 타입 변경은 롤백이 불가능합니다. 셋째, 구버전 애플리케이션이 삭제된 컬럼을 참조하면 즉시 오류가 발생합니다. 이 세 가지 문제를 모두 해결하는 패턴이 Expand-Contract입니다.

ALTER TABLE users DROP COLUMN username을 운영 DB에 실행했는데, 아직 구버전 애플리케이션 인스턴스가 username 컬럼을 참조하고 있어 즉시 500 오류가 발생했습니다. DDL 롤백은 불가능하므로 데이터를 복구하려면 백업에서 복원해야 합니다.

해결: 컬럼 삭제는 절대 단독으로 실행하지 않습니다. Expand-Contract 패턴의 3단계(새 컬럼 추가 → 코드 배포 → 구 컬럼 삭제)를 반드시 준수하고, 각 단계 사이에 최소 한 번의 전체 배포 주기를 가져야 합니다.

Expand-Contract 패턴

운영 중인 서비스에서 스키마를 변경할 때는 Expand-Contract(확장-수축) 패턴을 사용합니다. 이 패턴은 하나의 변경을 3단계로 나눠서 서비스 중단 없이 진행합니다.

시나리오: users.username 컬럼을 users.display_name으로 이름 변경

3단계로 나눕니다. 1단계에서는 새 컬럼을 추가하면서 기존 컬럼은 그대로 유지합니다. 그다음 애플리케이션 코드를 display_name에 쓰고 username에서 읽는 호환 로직으로 배포합니다. 2단계에서는 기존 데이터를 새 컬럼으로 이전하고, display_name만 사용하는 코드로 전환 배포합니다. 3단계에서는 구 컬럼이 더 이상 참조되지 않음을 확인한 뒤 삭제합니다.

위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE users ADD COLUMN display_name VARCHAR(100);
SQL
UPDATE users SET display_name = username WHERE display_name IS NULL;
위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE users DROP COLUMN username;

NOT NULL 컬럼 추가 시 주의사항

기존 데이터가 있는 테이블에 NOT NULL 컬럼을 즉시 추가하면 기존 행이 NULL 상태가 되어 제약 위반 오류가 발생합니다. 올바른 방법은 3단계로 나누는 것입니다.

위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE orders ADD COLUMN shipping_address TEXT;
SQL
UPDATE orders SET shipping_address = '주소 미등록' WHERE shipping_address IS NULL;
위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE orders ALTER COLUMN shipping_address SET NOT NULL;
1NOT NULL 컬럼을 '즉시' 추가해 실패시키고, 3단계로 안전하게 추가

기존 행이 있는 테이블에 NOT NULL을 한 번에 거는 게 왜 위험한지 직접 부딪힙니다. 즉시 SET NOT NULL이 기존 NULL 행 때문에 거부되는 걸 본 뒤, 추가→백필→제약의 3단계로 통과시킵니다.

SQL
-- (실패) 기존 행 shipping_address가 NULL인 상태에서 바로 NOT NULL
ALTER TABLE orders ALTER COLUMN shipping_address SET NOT NULL;

-- (안전) 1) nullable로 컬럼 추가  2) 기존 행 백필  3) 그제서야 NOT NULL
ALTER TABLE orders ADD COLUMN shipping_address TEXT;          -- 1
UPDATE orders SET shipping_address = '주소 미등록'
  WHERE shipping_address IS NULL;                              -- 2
ALTER TABLE orders ALTER COLUMN shipping_address SET NOT NULL; -- 3

-- 백필 누락 검증: 0이어야 3단계가 성공한다
SELECT count(*) FROM orders WHERE shipping_address IS NULL;
OUTPUT
-- 즉시 SET NOT NULL
ERROR:  column "shipping_address" of relation "orders" contains null values

-- 3단계 후 검증
 count
-------
     0
ALTER TABLE orders ALTER COLUMN shipping_address SET NOT NULL;
🔍실행 후 확인할 것
  • 즉시 SET NOT NULL에서 'contains null values' 에러가 나는지 본다 — 나야 정상이다(기존 행이 NULL이라 제약을 못 건다는 증거)
  • 3단계 직전 SELECT count(*) ... IS NULL 이 0인지 반드시 확인 — 0이 아니면 백필이 덜 된 것이고, 이 상태로 SET NOT NULL하면 다시 실패한다
  • 대용량 테이블이면 2)번 UPDATE가 전체 행을 잠글 수 있다 — 배치(WHERE id BETWEEN ...)로 나눠 백필하는지 점검
  • PostgreSQL 11+는 'ADD COLUMN ... NOT NULL DEFAULT ...' 한 줄이 테이블 재작성 없이 끝난다 — 신규 컬럼이면 3단계 대신 이 방식을 우선 고려

PostgreSQL 11 이상에서는 DEFAULT 값이 있는 NOT NULL 컬럼을 즉시 추가해도 테이블 재작성 없이 처리됩니다.

위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE orders
    ADD COLUMN shipping_address TEXT NOT NULL DEFAULT '주소 미등록';

대용량 테이블 인덱스 CONCURRENTLY

일반 CREATE INDEX는 테이블 전체 잠금을 걸어 수백만 건 이상의 테이블에서는 수분간 서비스가 중단될 수 있습니다. 운영 환경에서는 항상 CONCURRENTLY 옵션을 사용합니다.

CONCURRENTLY는 트랜잭션 블록 내에서 사용할 수 없으므로 BEGIN/COMMIT 밖에서 단독으로 실행해야 합니다. 실패 시 INVALID 상태의 인덱스가 남을 수 있으며, 이 경우 DROP INDEX CONCURRENTLY로 정리한 후 재시도합니다.

SQL
CREATE INDEX CONCURRENTLY idx_orders_user_id ON orders(user_id);
SQL
DROP INDEX CONCURRENTLY idx_orders_user_id;

Flyway에서 무중단 마이그레이션 — 배치 업데이트 예시

컬럼을 추가한 뒤 기존 데이터를 채울 때 한 번에 전체 행을 업데이트하면 테이블 잠금이 발생할 수 있습니다. 아래는 1,000건씩 나눠서 처리하는 배치 방식입니다.

위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE users ADD COLUMN tier VARCHAR(20);
SQL
DO $$
DECLARE
    batch_size INT := 1000;
    updated    INT;
BEGIN
    LOOP
        UPDATE users
        SET tier = 'standard'
        WHERE id IN (
            SELECT id FROM users
            WHERE tier IS NULL
            ORDER BY id
            LIMIT batch_size
        );

        GET DIAGNOSTICS updated = ROW_COUNT;
        EXIT WHEN updated = 0;
        PERFORM pg_sleep(0.1);
    END LOOP;
END $$;
위험 명령어

운영 데이터에 적용하면 되돌리기 어려운 변경입니다. 실행 전 대상 테이블, WHERE 조건, 백업 또는 롤백 경로를 반드시 확인하세요.

SQL
ALTER TABLE users ALTER COLUMN tier SET NOT NULL;
ALTER TABLE users ALTER COLUMN tier SET DEFAULT 'standard';
💼
실무 맥락무중단 배포 파이프라인에서 스키마 변경 자동화
현업 패턴

팀이 GitHub Actions 또는 Jenkins 파이프라인에서 Flyway를 사용할 때, 마이그레이션 단계를 애플리케이션 배포 단계와 분리합니다. 일반적인 파이프라인은 "1단계: flyway migrate 실행 → 2단계: 헬스 체크 → 3단계: 앱 롤링 배포" 순서입니다. Expand-Contract 패턴을 따르면 마이그레이션 파일이 먼저 적용된 상태에서도 구버전 앱이 정상 동작하므로 롤링 배포 중 오류가 발생하지 않습니다. 반대로 컬럼 삭제처럼 하위 호환이 깨지는 변경은 반드시 앱 배포가 100% 완료된 뒤 별도 마이그레이션으로 실행해야 합니다.

체크리스트: 운영 마이그레이션 전 확인 사항

항목확인
마이그레이션이 트랜잭션으로 감싸져 있는가?실패 시 롤백 보장
대용량 테이블 인덱스에 CONCURRENTLY 사용했는가?잠금 방지
NOT NULL 추가 전 데이터를 채웠는가?오류 방지
스테이징 환경에서 먼저 테스트했는가?실행 시간 파악
롤백 계획이 있는가?사고 대비
마이그레이션 실행 중 모니터링 준비했는가?이상 감지

심화 — 백필 UPDATE의 청구서: dead tuple·autovacuum·복제 지연

💡개념

심화: '한 방 UPDATE'가 왜 위험한가 — 배치가 선택이 아닌 이유

앞의 배치 백필 예제는 한 번에 다 바꾸지 말고 수천 건씩이라고 했습니다. 왜 그래야 하는지 한 단계 더 들어가 보면, 마이그레이션이 락을 안 잡아도 다른 방식으로 서비스를 흔들 수 있음을 알게 됩니다.

  • UPDATE는 행을 덮어쓰지 않습니다: PostgreSQL은 MVCC라 한 행을 UPDATE하면 새 버전을 쓰고 옛 버전을 dead tuple로 남깁니다. 5천만 행을 한 UPDATE로 바꾸면 5천만 개의 dead tuple이 한꺼번에 생깁니다. 테이블은 순식간에 부풀고, 이후 순차 스캔이 죽은 공간까지 읽느라 느려집니다.
  • 긴 트랜잭션은 autovacuum을 무력화합니다: 그 거대한 UPDATE가 도는 동안(그리고 커밋될 때까지) xmin horizon이 묶여 autovacuum이 dead tuple을 회수하지 못합니다. 끝난 뒤에도 autovacuum이 수천만 행을 정리하느라 몇 시간 CPU·I/O를 잡아, 그 시간 내내 운영 쿼리와 자원을 다툽니다.
  • 복제본이 뒤처집니다: 대량 변경은 그만큼의 WAL을 폭발적으로 만들고, 복제본은 이를 대체로 직렬로 재생합니다. 그 결과 read replica가 몇 분씩 지연돼, 쓰고 바로 읽기가 복제본으로 가는 요청에서 stale read가 번집니다(Master-Slave 복제(Replication) 구축과 DB 고가용성(HA) 아키텍처). 마이그레이션 하나가 읽기 경로 전체의 신선도를 흔드는 것입니다.
  • 그래서 배치로 나눕니다: 수천 건씩 끊어 커밋하면 트랜잭션이 짧아 xmin horizon이 자주 풀리고, autovacuum이 사이사이 dead tuple을 회수하며, WAL과 복제 지연이 완만해집니다. 배치 사이 짧은 pg_sleep은 복제본과 autovacuum이 따라올 여유를 줍니다. 느리게 하는 게 아니라 흔들지 않게 하는 것입니다.

핵심은, 마이그레이션의 위험이 락만이 아니라는 점입니다. 락을 피해도 dead tuple·autovacuum·복제 지연이라는 청구서가 뒤따라오며, 배치는 그 청구서를 잘게 쪼개는 장치입니다.

상황: 신규 컬럼을 채우려고 대용량 테이블 전체를 한 문장의 UPDATE로 백필했습니다. 마이그레이션 자체는 성공으로 끝났는데, 이후 복제본을 보는 화면들이 옛 데이터를 보여주고 원본 DB의 그 테이블 조회도 눈에 띄게 느려졌습니다.

원인: 한 방 UPDATE가 수천만 개의 dead tuple을 만들어 테이블이 급팽창했고, 그 큰 변경이 만든 WAL을 복제본이 직렬로 재생하느라 수 분씩 뒤처졌습니다. 동시에 autovacuum이 방대한 dead tuple을 정리하며 자원을 잡아 조회까지 느려졌습니다. 락은 없었지만 MVCC·복제·autovacuum 쪽으로 비용이 청구된 것입니다.

진단: pg_stat_user_tables에서 그 테이블의 n_dead_tup 급증과 테이블 크기 증가를 확인합니다. 복제 지연 지표가 UPDATE 시점부터 치솟았는지 봅니다. autovacuum이 그 테이블에 장시간 돌고 있는지 pg_stat_activity로 확인하면 그림이 맞아떨어집니다.

해결: 대량 백필은 처음부터 배치로 나눠 수천 건씩 커밋하고 사이에 짧은 대기를 둬, dead tuple 회수·WAL·복제 지연을 완만하게 만듭니다. 이미 팽창했다면 한적한 시간에 VACUUM으로 공간을 회수합니다. 복제 지연에 민감한 쓰고 바로 읽기 요청은 백필 동안 주 인스턴스로 보내고, 대규모 백필은 트래픽이 낮은 시간대에 예약합니다(대량 데이터 고성능 처리 패턴 Bulk Insert·Update·Delete).


명령어·구문 빠른 참조

이 모듈에서 다룬 마이그레이션 도구 명령과 무중단 DDL 구문을 실전 예와 함께 모았습니다.

구문/명령용도
Flyway V{n}__desc.sql버전 번호 붙은 마이그레이션 파일V2__add_email_to_users.sql (적용 후 수정 금지)
Flyway R__desc.sql내용 바뀔 때마다 재실행(뷰·함수)R__create_user_summary_view.sql
flyway_schema_history 조회적용 이력·체크섬·성공 여부 확인SELECT version, checksum, success FROM flyway_schema_history ORDER BY installed_rank;
prisma migrate dev / deploy개발 생성 / 운영 적용npx prisma migrate deploy, ... migrate status
ALTER TABLE ADD COLUMN컬럼 추가(1단계는 nullable로)ALTER TABLE users ADD COLUMN display_name VARCHAR(100);
UPDATE ... WHERE ... IS NULL기존 행 백필(2단계)UPDATE users SET display_name = username WHERE display_name IS NULL;
ALTER COLUMN ... SET NOT NULL백필 후 제약 추가(3단계)즉시 NOT NULL은 기존 NULL 행 때문에 실패
ADD COLUMN ... NOT NULL DEFAULTPG 11+ 테이블 재작성 없는 컬럼 추가... ADD COLUMN tier TEXT NOT NULL DEFAULT 'standard';
CREATE INDEX CONCURRENTLY운영 테이블 락 없이 인덱스 생성트랜잭션 블록 밖에서 단독 실행
DROP INDEX CONCURRENTLY실패로 남은 INVALID 인덱스 정리DROP INDEX CONCURRENTLY idx_orders_user_id;
배치 백필 DO $$ ... LOOP대량 UPDATE를 나눠 커밋LIMIT 1000 + GET DIAGNOSTICS + PERFORM pg_sleep(0.1)
백필 검증 SELECT count(*)NULL 잔여 0 확인 후 제약 적용SELECT count(*) FROM orders WHERE shipping_address IS NULL;
Expand-Contract 3단계무중단 스키마 변경 순서새 컬럼 추가 → 코드 배포 → 구 컬럼 삭제

관련 모듈로 더 깊이:

다음 모듈에서는 PostgreSQL의 JSONB 비정형 데이터 처리와 전문 검색(Full-text Search) 기능을 다룹니다.

지식 확인

퀴즈 — 8문제

Q1

팀원이 실수로 이미 운영 DB에 적용된 V3__add_column.sql 파일을 수정하고 커밋했다. 다음 배포 때 Flyway가 이 파일을 실행하면 어떻게 되는가?

Q2

Expand-Contract 패턴에서 NOT NULL 컬럼을 추가할 때 올바른 순서는?

Q3

같은 스키마 변경을 Oracle, PostgreSQL, MySQL 세 환경에 모두 배포해야 한다. SQL 방언(dialect) 차이 때문에 환경마다 다른 SQL을 유지하기 어렵다. 하나의 변경 정의로 여러 DB를 지원하고 싶다면 어떤 도구가 더 적합한가?

Q4

대용량 테이블에서 인덱스를 추가할 때 서비스 중단 없이 실행하는 PostgreSQL 명령은?

Q5

Flyway로 관리하는 CREATE OR REPLACE VIEW 정의가 자주 바뀐다. 현재 V5__create_view.sql로 관리하는데 수정할 때마다 V6__, V7__... 파일을 계속 추가해야 해서 번거롭다. 더 나은 방법은?

Q6

DB 스키마 변경을 'DBA가 운영 DB에 직접 SQL 실행'이 아니라 Flyway/Liquibase 같은 마이그레이션 도구로 버전 관리하는 이유는?

Q7

[심화] 대용량 테이블 전체를 한 문장의 UPDATE로 백필하면, 테이블 락을 오래 잡지 않아도 서비스에 부담을 주는 주된 이유는?

Q8

[심화] 대량 백필을 수천 건씩 끊어 커밋하고 배치 사이에 짧은 대기를 두는 방식이 주는 이점은?

0 / 8 답변

🧪 실습으로 확인하기

PostgreSQL 설치 및 기본 설정

초급

Linux 서버에 PostgreSQL을 설치하고, 데이터베이스와 사용자를 생성한 뒤 외부 접속이 가능하도록 설정한다.

50📋 5단계💻 직접 환경
실습 시작하기 →

이것도 배워보세요