- Published on
Flyway & DB 마이그레이션 완전 가이드 2025: 스키마 버전 관리, ORM 통합, 무중단 배포까지
- Authors

- Name
- Youngju Kim
- @fjvbn20031
1. 왜 데이터베이스 마이그레이션이 필요한가
"내 로컬에서는 되는데" 문제
코드는 Git으로 완벽하게 버전 관리하면서, 데이터베이스 스키마는 수동으로 ALTER TABLE을 실행하고 있다면 심각한 문제가 존재합니다.
개발자 A: "users 테이블에 phone 컬럼 추가했어요"
개발자 B: "어? 제 로컬에는 없는데요?"
개발자 A: "슬랙에 공유했잖아요..."
개발자 C: "스테이징은요? 프로덕션은?"
(3시간 후 장애 발생)
이런 시나리오는 실제로 수많은 팀에서 반복됩니다. 코드와 스키마가 동기화되지 않으면 배포 시 장애가 발생하고, 롤백도 어렵습니다.
수동 ALTER TABLE의 공포
-- 금요일 오후 6시, 프로덕션에서 직접 실행...
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE orders MODIFY COLUMN status ENUM('pending','processing','shipped','delivered','cancelled');
-- 실수로 WHERE 없이...
UPDATE users SET role = 'admin';
-- Ctrl+C... 이미 늦었다
수동 관리의 문제점은 다음과 같습니다.
- 누가, 언제, 무엇을 변경했는지 추적 불가
- 환경(로컬/스테이징/프로덕션)마다 스키마가 다름
- 롤백 절차가 없음
- 코드 리뷰 없이 직접 실행
- 실행 순서 보장이 안 됨
데이터베이스 버전 관리: Git과 같은 원칙
코드에 Git을 사용하는 것처럼, DB 스키마에도 버전 관리가 필요합니다.
Git으로 코드 관리 DB 마이그레이션으로 스키마 관리
------------------ ---------------------------
commit history → migration history
branch/merge → migration branching
code review (PR) → migration review
rollback (revert) → undo migration
CI/CD pipeline → migration in pipeline
핵심 원칙은 다음과 같습니다.
- 모든 스키마 변경은 마이그레이션 파일로 - 직접 SQL 실행 금지
- 마이그레이션은 코드와 함께 버전 관리 - Git 저장소에 포함
- 자동화된 실행 - CI/CD 파이프라인에서 실행
- 멱등성(Idempotency) - 같은 마이그레이션을 여러 번 실행해도 동일 결과
- 순서 보장 - 마이그레이션은 정해진 순서대로 실행
2. 마이그레이션 도구 비교
주요 도구 비교표
| 도구 | 접근 방식 | 언어/생태계 | 강점 | 약점 |
|---|---|---|---|---|
| Flyway | 버전 기반 SQL | Java/CLI | 단순, SQL 네이티브 | 유료 기능 많음 |
| Liquibase | 변경셋 XML/YAML/SQL | Java/CLI | 롤백, diff | 설정 복잡 |
| Prisma Migrate | 선언적 스키마 | TypeScript | 타입 안전, Shadow DB | Prisma 종속 |
| TypeORM | 코드 기반 마이그레이션 | TypeScript | 엔티티 자동 생성 | TypeORM 종속 |
| Alembic | 버전 기반 Python | Python | SQLAlchemy 통합 | Python 전용 |
| Django Migrations | 자동 감지 | Python | ORM 완전 통합 | Django 전용 |
| Knex.js | 코드 기반 | JavaScript | 경량 | 기능 제한적 |
| golang-migrate | 버전 기반 SQL | Go/CLI | 경량, 다중 DB | 기능 최소화 |
선택 기준
프로젝트 기술 스택이 무엇인가?
├── Java/Spring → Flyway 또는 Liquibase
│ ├── 단순함 선호 → Flyway
│ └── 롤백/diff 필요 → Liquibase
├── TypeScript/Node.js
│ ├── Prisma 사용 → Prisma Migrate
│ ├── TypeORM 사용 → TypeORM Migration
│ └── 경량 → Knex.js
├── Python
│ ├── Django → Django Migrations
│ └── Flask/FastAPI → Alembic
├── Go → golang-migrate
└── 언어 무관, SQL 직접 → Flyway CLI
3. Flyway 심층 분석
3.1 Flyway 소개
Flyway는 가장 널리 사용되는 데이터베이스 마이그레이션 도구입니다. "단순함"을 핵심 철학으로 삼아, SQL 파일에 버전 번호를 붙여 순서대로 실행하는 직관적인 방식입니다.
flyway_migrations/
├── V1__Create_users_table.sql
├── V2__Create_orders_table.sql
├── V3__Add_email_to_users.sql
├── V4__Create_products_table.sql
├── R__Create_views.sql # Repeatable
└── U3__Add_email_to_users.sql # Undo (Teams)
3.2 설치 방법
CLI 설치 (macOS)
# Homebrew
brew install flyway
# 버전 확인
flyway --version
# Flyway Community Edition 10.x.x
Docker
docker run --rm \
-v $(pwd)/sql:/flyway/sql \
-v $(pwd)/conf:/flyway/conf \
flyway/flyway:10-alpine \
migrate
Maven 플러그인
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>10.8.1</version>
<configuration>
<url>jdbc:postgresql://localhost:5432/mydb</url>
<user>postgres</user>
<password>secret</password>
<locations>
<location>filesystem:src/main/resources/db/migration</location>
</locations>
</configuration>
</plugin>
Gradle 플러그인
plugins {
id 'org.flywaydb.flyway' version '10.8.1'
}
flyway {
url = 'jdbc:postgresql://localhost:5432/mydb'
user = 'postgres'
password = 'secret'
locations = ['filesystem:src/main/resources/db/migration']
}
3.3 네이밍 컨벤션
Flyway의 마이그레이션 파일 이름 규칙은 다음과 같습니다.
V{version}__{description}.sql # Versioned Migration
R__{description}.sql # Repeatable Migration
U{version}__{description}.sql # Undo Migration (Teams)
규칙의 핵심 포인트는 다음과 같습니다.
- 접두사: V(Versioned), R(Repeatable), U(Undo)
- 버전: 숫자(1, 2, 3) 또는 날짜(20250325) 또는 타임스탬프
- 구분자: 언더스코어 두 개(
__) - 설명: 영문, 언더스코어로 구분
-- V1__Create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_username ON users(username);
-- V2__Create_orders_table.sql
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total_amount DECIMAL(10,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
-- V3__Add_phone_to_users.sql
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN profile_image_url TEXT;
-- R__Create_reporting_views.sql (매번 재실행됨)
CREATE OR REPLACE VIEW v_user_order_summary AS
SELECT
u.id AS user_id,
u.username,
u.email,
COUNT(o.id) AS order_count,
COALESCE(SUM(o.total_amount), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.username, u.email;
3.4 주요 커맨드
# 마이그레이션 실행
flyway migrate
# 현재 상태 확인
flyway info
# 마이그레이션 유효성 검증
flyway validate
# 데이터베이스 초기화 (주의: 모든 객체 삭제)
flyway clean
# 기존 DB에 Flyway 적용 (베이스라인)
flyway baseline
# 실패한 마이그레이션 복구
flyway repair
info 출력 예시
+-----------+---------+---------------------+------+---------------------+---------+----------+
| Category | Version | Description | Type | Installed On | State | Undoable |
+-----------+---------+---------------------+------+---------------------+---------+----------+
| Versioned | 1 | Create users table | SQL | 2025-03-20 10:15:30 | Success | No |
| Versioned | 2 | Create orders table | SQL | 2025-03-20 10:15:31 | Success | No |
| Versioned | 3 | Add phone to users | SQL | | Pending | No |
+-----------+---------+---------------------+------+---------------------+---------+----------+
3.5 설정 파일
flyway.conf (전통 방식)
# 데이터베이스 연결
flyway.url=jdbc:postgresql://localhost:5432/mydb
flyway.user=postgres
flyway.password=secret
# 마이그레이션 위치
flyway.locations=filesystem:./sql,classpath:db/migration
# 스키마
flyway.schemas=public,app
flyway.defaultSchema=app
# 테이블
flyway.table=flyway_schema_history
# 인코딩
flyway.encoding=UTF-8
# 플레이스홀더
flyway.placeholders.env=production
flyway.placeholders.schema_name=app
# 검증
flyway.validateOnMigrate=true
flyway.validateMigrationNaming=true
# 기타
flyway.outOfOrder=false
flyway.baselineOnMigrate=false
flyway.cleanDisabled=true
flyway.toml (Flyway v10 신규 형식)
[environments.default]
url = "jdbc:postgresql://localhost:5432/mydb"
user = "postgres"
password = "secret"
[flyway]
locations = ["filesystem:./sql"]
defaultSchema = "app"
table = "flyway_schema_history"
validateOnMigrate = true
cleanDisabled = true
[flyway.placeholders]
env = "production"
schema_name = "app"
3.6 플레이스홀더와 환경별 설정
-- V5__Create_audit_table.sql
CREATE TABLE ${schema_name}.audit_log (
id BIGSERIAL PRIMARY KEY,
table_name VARCHAR(100) NOT NULL,
operation VARCHAR(10) NOT NULL,
old_data JSONB,
new_data JSONB,
changed_by VARCHAR(100),
changed_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- 환경별 파티션
-- ${env} 플레이스홀더 사용
COMMENT ON TABLE ${schema_name}.audit_log IS 'Audit log for ${env} environment';
환경별 설정 파일은 다음과 같습니다.
# flyway-dev.conf
flyway.url=jdbc:postgresql://localhost:5432/mydb_dev
flyway.placeholders.env=development
# flyway-staging.conf
flyway.url=jdbc:postgresql://staging-db:5432/mydb
flyway.placeholders.env=staging
# flyway-prod.conf
flyway.url=jdbc:postgresql://prod-db:5432/mydb
flyway.placeholders.env=production
flyway.cleanDisabled=true
# 환경별 실행
flyway -configFiles=flyway-dev.conf migrate
flyway -configFiles=flyway-staging.conf migrate
flyway -configFiles=flyway-prod.conf migrate
3.7 콜백
Flyway는 마이그레이션 전후에 실행되는 콜백을 지원합니다.
콜백 파일 이름:
beforeMigrate.sql - migrate 실행 전
beforeEachMigrate.sql - 각 마이그레이션 전
afterEachMigrate.sql - 각 마이그레이션 후
afterMigrate.sql - migrate 완료 후
beforeClean.sql - clean 실행 전
afterClean.sql - clean 완료 후
beforeInfo.sql - info 실행 전
afterInfo.sql - info 완료 후
beforeValidate.sql - validate 실행 전
afterValidate.sql - validate 완료 후
-- afterMigrate.sql
-- 마이그레이션 완료 후 통계 뷰 갱신
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_daily_stats;
ANALYZE users;
ANALYZE orders;
-- 마이그레이션 이력 로깅
INSERT INTO migration_audit_log (action, executed_at)
VALUES ('migration_completed', NOW());
3.8 Java 기반 마이그레이션
복잡한 로직이 필요한 경우 Java 코드로 마이그레이션을 작성할 수 있습니다.
// V6__Encrypt_user_emails.java
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.*;
public class V6__Encrypt_user_emails extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
Connection conn = context.getConnection();
// 배치 처리로 대량 데이터 암호화
String selectSql = "SELECT id, email FROM users WHERE encrypted_email IS NULL";
String updateSql = "UPDATE users SET encrypted_email = ? WHERE id = ?";
try (Statement select = conn.createStatement();
PreparedStatement update = conn.prepareStatement(updateSql)) {
ResultSet rs = select.executeQuery(selectSql);
int batchSize = 0;
while (rs.next()) {
long id = rs.getLong("id");
String email = rs.getString("email");
String encrypted = encrypt(email);
update.setString(1, encrypted);
update.setLong(2, id);
update.addBatch();
if (++batchSize % 1000 == 0) {
update.executeBatch();
}
}
if (batchSize % 1000 != 0) {
update.executeBatch();
}
}
}
private String encrypt(String value) {
// 실제 암호화 로직 구현
return "ENC:" + value; // 예시
}
}
3.9 Spring Boot 통합
# application.yml
spring:
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: '0'
validate-on-migrate: true
out-of-order: false
table: flyway_schema_history
schemas:
- public
placeholders:
env: ${SPRING_PROFILES_ACTIVE:dev}
clean-disabled: true
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: secret
// FlywayConfig.java
@Configuration
public class FlywayConfig {
@Bean
public FlywayMigrationStrategy flywayMigrationStrategy() {
return flyway -> {
// 마이그레이션 정보 출력
MigrationInfo[] pending = flyway.info().pending();
if (pending.length > 0) {
System.out.println("Pending migrations: " + pending.length);
for (MigrationInfo info : pending) {
System.out.println(" - " + info.getVersion()
+ ": " + info.getDescription());
}
}
flyway.migrate();
};
}
}
3.10 전체 프로젝트 예시 (PostgreSQL)
my-app/
├── src/main/resources/
│ └── db/
│ └── migration/
│ ├── V1__Create_schema.sql
│ ├── V2__Create_users.sql
│ ├── V3__Create_products.sql
│ ├── V4__Create_orders.sql
│ ├── V5__Add_indexes.sql
│ ├── V6__Create_audit_triggers.sql
│ └── R__Update_views.sql
├── flyway.toml
├── pom.xml
└── docker-compose.yml
# docker-compose.yml
version: '3.8'
services:
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: secret
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
flyway:
image: flyway/flyway:10-alpine
depends_on:
- db
volumes:
- ./src/main/resources/db/migration:/flyway/sql
- ./flyway.toml:/flyway/flyway.toml
command: migrate
environment:
FLYWAY_URL: jdbc:postgresql://db:5432/myapp
FLYWAY_USER: postgres
FLYWAY_PASSWORD: secret
volumes:
pgdata:
4. Liquibase 비교
4.1 Liquibase 변경 로그 형식
Liquibase는 XML, YAML, SQL, JSON 네 가지 형식을 지원합니다.
YAML 형식 (가장 읽기 쉬움)
# db/changelog/db.changelog-master.yaml
databaseChangeLog:
- changeSet:
id: 1
author: dev-team
changes:
- createTable:
tableName: users
columns:
- column:
name: id
type: bigint
autoIncrement: true
constraints:
primaryKey: true
nullable: false
- column:
name: username
type: varchar(50)
constraints:
nullable: false
unique: true
- column:
name: email
type: varchar(255)
constraints:
nullable: false
unique: true
- column:
name: created_at
type: timestamp with time zone
defaultValueComputed: NOW()
rollback:
- dropTable:
tableName: users
- changeSet:
id: 2
author: dev-team
changes:
- addColumn:
tableName: users
columns:
- column:
name: phone
type: varchar(20)
rollback:
- dropColumn:
tableName: users
columnName: phone
SQL 형식
-- changeset dev-team:1
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- rollback DROP TABLE users;
-- changeset dev-team:2
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
-- rollback ALTER TABLE users DROP COLUMN phone;
4.2 Liquibase 주요 기능
# 마이그레이션 실행
liquibase update
# 롤백 (마지막 N개)
liquibase rollbackCount 1
# 롤백 (특정 태그까지)
liquibase rollback v1.0
# 현재 DB와 변경 로그 차이 확인
liquibase diff
# 기존 DB에서 변경 로그 자동 생성
liquibase generateChangeLog
# 상태 확인
liquibase status
4.3 Flyway vs Liquibase 선택 기준
Flyway 선택 시점:
- SQL을 직접 작성하고 싶을 때
- 단순한 워크플로우 선호
- Spring Boot 생태계 사용 시
- 팀이 SQL에 익숙할 때
Liquibase 선택 시점:
- 롤백이 필수일 때
- 다중 DB 벤더 지원 필요
- 기존 DB 스키마를 변경 로그로 변환할 때
- XML/YAML 기반 선언적 관리 선호 시
- 세밀한 컨텍스트/조건부 실행 필요 시
5. ORM 마이그레이션 시스템
5.1 Prisma Migrate (TypeScript/Node.js)
스키마 정의
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
phone String?
posts Post[]
orders Order[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int @map("author_id")
createdAt DateTime @default(now()) @map("created_at")
@@map("posts")
}
model Order {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int @map("user_id")
totalAmount Decimal @map("total_amount") @db.Decimal(10, 2)
status OrderStatus @default(PENDING)
items OrderItem[]
createdAt DateTime @default(now()) @map("created_at")
@@map("orders")
}
model OrderItem {
id Int @id @default(autoincrement())
order Order @relation(fields: [orderId], references: [id])
orderId Int @map("order_id")
product String
quantity Int
price Decimal @db.Decimal(10, 2)
@@map("order_items")
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
마이그레이션 명령어
# 개발 환경 마이그레이션 (생성 + 적용)
npx prisma migrate dev --name create_initial_schema
# 프로덕션 마이그레이션 적용
npx prisma migrate deploy
# 마이그레이션 초기화
npx prisma migrate reset
# 마이그레이션 상태 확인
npx prisma migrate status
# 스키마와 DB 차이 확인
npx prisma migrate diff \
--from-schema-datamodel prisma/schema.prisma \
--to-schema-datasource prisma/schema.prisma
생성된 마이그레이션 파일
-- prisma/migrations/20250325_create_initial_schema/migration.sql
CREATE TYPE "OrderStatus" AS ENUM ('PENDING', 'PROCESSING', 'SHIPPED', 'DELIVERED', 'CANCELLED');
CREATE TABLE "users" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
"phone" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "posts" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"author_id" INTEGER NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "posts_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
ALTER TABLE "posts"
ADD CONSTRAINT "posts_author_id_fkey"
FOREIGN KEY ("author_id") REFERENCES "users"("id")
ON DELETE RESTRICT ON UPDATE CASCADE;
Shadow Database 개념
Prisma Migrate는 Shadow Database를 사용하여 마이그레이션의 정확성을 검증합니다.
1. 임시 Shadow DB 생성
2. 모든 기존 마이그레이션을 Shadow DB에 적용
3. 새 마이그레이션 SQL 생성
4. Shadow DB에 새 마이그레이션 적용 검증
5. Shadow DB 삭제
5.2 TypeORM 마이그레이션
엔티티 정의
// src/entities/User.ts
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn, OneToMany
} from 'typeorm';
import { Order } from './Order';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 50, unique: true })
username: string;
@Column({ type: 'varchar', length: 255, unique: true })
email: string;
@Column({ type: 'varchar', length: 20, nullable: true })
phone: string;
@OneToMany(() => Order, order => order.user)
orders: Order[];
@CreateDateColumn({ name: 'created_at' })
createdAt: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date;
}
마이그레이션 생성 및 실행
# 엔티티 변경 감지하여 마이그레이션 자동 생성
npx typeorm migration:generate -d src/data-source.ts src/migrations/AddPhoneToUser
# 빈 마이그레이션 생성
npx typeorm migration:create src/migrations/SeedAdminUser
# 마이그레이션 실행
npx typeorm migration:run -d src/data-source.ts
# 마지막 마이그레이션 롤백
npx typeorm migration:revert -d src/data-source.ts
자동 생성된 마이그레이션 예시
// src/migrations/1711234567890-AddPhoneToUser.ts
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddPhoneToUser1711234567890 implements MigrationInterface {
name = 'AddPhoneToUser1711234567890';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "users" ADD "phone" varchar(20)`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "users" DROP COLUMN "phone"`
);
}
}
5.3 Alembic (Python/SQLAlchemy)
초기 설정
# Alembic 설치
pip install alembic sqlalchemy psycopg2-binary
# 프로젝트 초기화
alembic init alembic
# alembic/env.py (핵심 부분)
from myapp.models import Base
target_metadata = Base.metadata
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
마이그레이션 생성 및 실행
# 자동 생성 (모델 변경 감지)
alembic revision --autogenerate -m "add phone to users"
# 수동 생성
alembic revision -m "seed initial data"
# 최신 버전으로 업그레이드
alembic upgrade head
# 특정 버전으로 업그레이드
alembic upgrade abc123
# 롤백 (한 단계)
alembic downgrade -1
# 현재 상태 확인
alembic current
# 마이그레이션 이력
alembic history
자동 생성된 마이그레이션
# alembic/versions/abc123_add_phone_to_users.py
"""add phone to users"""
from alembic import op
import sqlalchemy as sa
revision = 'abc123'
down_revision = 'prev456'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('users',
sa.Column('phone', sa.String(20), nullable=True)
)
op.create_index('ix_users_phone', 'users', ['phone'])
def downgrade():
op.drop_index('ix_users_phone', 'users')
op.drop_column('users', 'phone')
5.4 Django 마이그레이션
# models.py
from django.db import models
class User(models.Model):
username = models.CharField(max_length=50, unique=True)
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'users'
# 마이그레이션 생성
python manage.py makemigrations
# 마이그레이션 적용
python manage.py migrate
# 마이그레이션 상태 확인
python manage.py showmigrations
# SQL 미리보기
python manage.py sqlmigrate myapp 0002
# 마이그레이션 스쿼시 (합치기)
python manage.py squashmigrations myapp 0001 0005
데이터 마이그레이션
# migrations/0003_populate_default_roles.py
from django.db import migrations
def create_roles(apps, schema_editor):
Role = apps.get_model('myapp', 'Role')
roles = ['admin', 'editor', 'viewer']
for role_name in roles:
Role.objects.get_or_create(name=role_name)
def remove_roles(apps, schema_editor):
Role = apps.get_model('myapp', 'Role')
Role.objects.filter(name__in=['admin', 'editor', 'viewer']).delete()
class Migration(migrations.Migration):
dependencies = [
('myapp', '0002_create_role_table'),
]
operations = [
migrations.RunPython(create_roles, remove_roles),
]
6. 무중단 마이그레이션 전략
6.1 Expand-Contract 패턴 (가장 중요)
무중단 스키마 변경의 핵심 패턴입니다. 3단계로 나눠서 안전하게 변경합니다.
예시: username 컬럼을 first_name + last_name으로 분리
Phase 1 (Expand): 새 컬럼 추가, 양쪽 모두 쓰기
Phase 2 (Migrate): 데이터 이전, 읽기 전환
Phase 3 (Contract): 기존 컬럼 제거
Phase 1: Expand (확장)
-- Migration V10: Add new columns
ALTER TABLE users ADD COLUMN first_name VARCHAR(50);
ALTER TABLE users ADD COLUMN last_name VARCHAR(50);
-- 트리거로 양방향 동기화
CREATE OR REPLACE FUNCTION sync_user_names()
RETURNS TRIGGER AS $func$
BEGIN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
IF NEW.first_name IS NULL AND NEW.username IS NOT NULL THEN
NEW.first_name := split_part(NEW.username, ' ', 1);
NEW.last_name := split_part(NEW.username, ' ', 2);
ELSIF NEW.username IS NULL AND NEW.first_name IS NOT NULL THEN
NEW.username := NEW.first_name || ' ' || COALESCE(NEW.last_name, '');
END IF;
END IF;
RETURN NEW;
END;
$func$ LANGUAGE plpgsql;
CREATE TRIGGER trg_sync_user_names
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION sync_user_names();
이 시점에서 배포: 새 코드는 first_name/last_name에 쓰기
이전 코드는 username에 쓰기 (트리거가 동기화)
Phase 2: Migrate (데이터 이전)
-- Migration V11: Backfill existing data
UPDATE users
SET first_name = split_part(username, ' ', 1),
last_name = split_part(username, ' ', 2)
WHERE first_name IS NULL;
-- NOT NULL 제약 추가
ALTER TABLE users ALTER COLUMN first_name SET NOT NULL;
Phase 3: Contract (축소)
-- Migration V12: Remove old column (모든 코드가 전환된 후)
DROP TRIGGER IF EXISTS trg_sync_user_names ON users;
DROP FUNCTION IF EXISTS sync_user_names();
ALTER TABLE users DROP COLUMN username;
6.2 컬럼 이름 변경 (무중단)
컬럼 이름을 직접 변경하면 서비스가 중단됩니다. 안전한 방법은 다음과 같습니다.
단계 1: 새 컬럼 추가 + 트리거
단계 2: 데이터 복사
단계 3: 코드에서 새 컬럼 사용
단계 4: 이전 컬럼 제거
6.3 NOT NULL 제약 안전하게 추가
-- 위험: 전체 테이블 잠금
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
-- 안전: PostgreSQL 12+ CHECK 제약 사용
ALTER TABLE users ADD CONSTRAINT users_email_not_null
CHECK (email IS NOT NULL) NOT VALID;
-- 별도 트랜잭션에서 검증 (AccessExclusiveLock 없이)
ALTER TABLE users VALIDATE CONSTRAINT users_email_not_null;
-- 그 후 NOT NULL 제약으로 전환
ALTER TABLE users ALTER COLUMN email SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT users_email_not_null;
6.4 대규모 테이블 ALTER TABLE
MySQL: gh-ost (GitHub Online Schema Change)
gh-ost \
--host=db-primary \
--database=myapp \
--table=users \
--alter="ADD COLUMN phone VARCHAR(20)" \
--execute \
--allow-on-master \
--chunk-size=1000 \
--max-load=Threads_running=25 \
--critical-load=Threads_running=100
MySQL: pt-online-schema-change (Percona Toolkit)
pt-online-schema-change \
--alter "ADD COLUMN phone VARCHAR(20)" \
--host=db-primary \
--user=admin \
--ask-pass \
--chunk-size=500 \
--max-lag=1s \
--execute \
D=myapp,t=users
PostgreSQL: CREATE INDEX CONCURRENTLY
-- 위험: 테이블 잠금
CREATE INDEX idx_users_email ON users(email);
-- 안전: 동시 인덱스 생성 (잠금 없음)
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
-- 주의: 트랜잭션 내에서 사용 불가
-- Flyway에서 사용시 트랜잭션 비활성화 필요
-- Flyway에서 CONCURRENTLY 사용
-- V15__Add_index_concurrently.sql
-- Flyway 트랜잭션 비활성화 (파일 상단 주석)
-- flyway:executeInTransaction=false
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email
ON users(email);
6.5 외래 키 안전하게 추가
-- 위험: 전체 테이블 스캔 + 잠금
ALTER TABLE orders ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id);
-- 안전: NOT VALID로 추가 후 별도 검증
ALTER TABLE orders ADD CONSTRAINT fk_orders_user
FOREIGN KEY (user_id) REFERENCES users(id)
NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_user;
7. 롤백 전략
7.1 Forward-Only vs 롤백 지원
Forward-Only (Flyway 기본):
V1 → V2 → V3 → V4
문제 발생 시 V5로 수정
Rollback 지원 (Liquibase, Flyway Teams):
V1 → V2 → V3 → V4
문제 발생 시 V3으로 되돌림
7.2 Flyway Undo 마이그레이션 (Teams 에디션)
-- U4__Remove_status_column.sql (V4의 Undo)
ALTER TABLE orders DROP COLUMN IF EXISTS status;
# Undo 실행
flyway undo
# 특정 버전까지 Undo
flyway undo -target=2
7.3 보상 마이그레이션 (Compensating Migration)
무료 버전에서 롤백이 필요한 경우 보상 마이그레이션을 작성합니다.
-- V4__Add_status_to_orders.sql (원래 마이그레이션)
ALTER TABLE orders ADD COLUMN status VARCHAR(20) DEFAULT 'pending';
-- V5__Revert_status_from_orders.sql (보상 마이그레이션)
ALTER TABLE orders DROP COLUMN IF EXISTS status;
7.4 롤백하면 안 되는 경우
롤백 금지 시나리오:
1. 컬럼 삭제 후 데이터 손실
- DROP COLUMN 후에는 데이터 복구 불가
- 반드시 백업 확인 후 삭제
2. 데이터 변환 후 원본 손실
- UPDATE로 데이터 형식 변경 후
- 원본 데이터를 보존하지 않은 경우
3. 대규모 데이터 마이그레이션
- 수백만 건 데이터 이동 후
- 롤백 시간이 너무 오래 걸림
4. 외부 시스템 연동
- API 스키마 변경 후 외부 시스템 적용
- 롤백해도 외부 시스템은 되돌릴 수 없음
8. CI/CD 통합
8.1 GitHub Actions 파이프라인
# .github/workflows/db-migration.yml
name: Database Migration
on:
push:
branches: [main]
paths:
- 'db/migration/**'
pull_request:
paths:
- 'db/migration/**'
jobs:
validate:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: testdb
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Run Flyway Validate
run: |
docker run --rm --network host \
-v $(pwd)/db/migration:/flyway/sql \
flyway/flyway:10-alpine \
-url=jdbc:postgresql://localhost:5432/testdb \
-user=postgres \
-password=testpass \
validate
- name: Run Flyway Info
run: |
docker run --rm --network host \
-v $(pwd)/db/migration:/flyway/sql \
flyway/flyway:10-alpine \
-url=jdbc:postgresql://localhost:5432/testdb \
-user=postgres \
-password=testpass \
info
- name: Test Migration
run: |
docker run --rm --network host \
-v $(pwd)/db/migration:/flyway/sql \
flyway/flyway:10-alpine \
-url=jdbc:postgresql://localhost:5432/testdb \
-user=postgres \
-password=testpass \
migrate
- name: Verify Schema
run: |
PGPASSWORD=testpass psql -h localhost -U postgres -d testdb -c "
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name;
"
deploy-staging:
needs: validate
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to Staging
run: |
docker run --rm \
-v $(pwd)/db/migration:/flyway/sql \
flyway/flyway:10-alpine \
-url=$STAGING_DB_URL \
-user=$STAGING_DB_USER \
-password=$STAGING_DB_PASSWORD \
migrate
env:
STAGING_DB_URL: ${{ secrets.STAGING_DB_URL }}
STAGING_DB_USER: ${{ secrets.STAGING_DB_USER }}
STAGING_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
deploy-production:
needs: deploy-staging
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to Production
run: |
docker run --rm \
-v $(pwd)/db/migration:/flyway/sql \
flyway/flyway:10-alpine \
-url=$PROD_DB_URL \
-user=$PROD_DB_USER \
-password=$PROD_DB_PASSWORD \
-validateOnMigrate=true \
migrate
env:
PROD_DB_URL: ${{ secrets.PROD_DB_URL }}
PROD_DB_USER: ${{ secrets.PROD_DB_USER }}
PROD_DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }}
8.2 Blue-Green 배포와 DB 마이그레이션
시간 흐름 →
1. [Blue 활성] DB v1, App v1
2. [Blue 활성] DB v1 → v2 마이그레이션 (하위 호환)
3. [Blue 활성] Green에 App v2 배포
4. [Blue → Green 전환] 트래픽 전환
5. [Green 활성] Blue 정리
핵심: DB 마이그레이션은 반드시 하위 호환이어야 함
이전 버전 코드가 새 스키마에서 동작해야 함
9. Kubernetes 환경
9.1 Init Container 방식
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
initContainers:
- name: flyway-migrate
image: flyway/flyway:10-alpine
args: ["migrate"]
env:
- name: FLYWAY_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: FLYWAY_USER
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
volumeMounts:
- name: migrations
mountPath: /flyway/sql
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
volumes:
- name: migrations
configMap:
name: flyway-migrations
9.2 Kubernetes Job 방식
# k8s/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: flyway-migrate-v10
labels:
app: flyway-migration
spec:
backoffLimit: 3
activeDeadlineSeconds: 300
template:
spec:
restartPolicy: Never
containers:
- name: flyway
image: flyway/flyway:10-alpine
args: ["migrate"]
env:
- name: FLYWAY_URL
valueFrom:
secretKeyRef:
name: db-secret
key: url
- name: FLYWAY_USER
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: password
- name: FLYWAY_VALIDATE_ON_MIGRATE
value: "true"
- name: FLYWAY_CONNECT_RETRIES
value: "5"
volumeMounts:
- name: migrations
mountPath: /flyway/sql
volumes:
- name: migrations
configMap:
name: flyway-migrations
9.3 Helm Hooks
# helm/templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}-flyway-migrate"
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-1"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: flyway
image: "flyway/flyway:{{ .Values.flyway.version }}"
args: ["migrate"]
env:
- name: FLYWAY_URL
value: "jdbc:postgresql://{{ .Values.db.host }}:{{ .Values.db.port }}/{{ .Values.db.name }}"
- name: FLYWAY_USER
valueFrom:
secretKeyRef:
name: "{{ .Values.db.secretName }}"
key: username
- name: FLYWAY_PASSWORD
valueFrom:
secretKeyRef:
name: "{{ .Values.db.secretName }}"
key: password
volumeMounts:
- name: migrations
mountPath: /flyway/sql
volumes:
- name: migrations
configMap:
name: "{{ .Release.Name }}-migrations"
9.4 동시 마이그레이션 처리 (잠금)
여러 Pod가 동시에 마이그레이션을 실행하는 것을 방지해야 합니다.
Flyway 내장 잠금:
- flyway_schema_history 테이블에 잠금 메커니즘 내장
- 첫 번째 인스턴스만 마이그레이션 실행
- 나머지는 대기 후 스킵
추가 안전장치:
- K8s Job (단일 Pod) 사용 권장
- Init Container 방식은 Pod마다 시도 (Flyway가 잠금 처리)
- DB Advisory Lock 활용
-- 커스텀 잠금 메커니즘 (Advisory Lock)
-- beforeMigrate.sql
SELECT pg_advisory_lock(12345);
-- afterMigrate.sql
SELECT pg_advisory_unlock(12345);
9.5 ArgoCD + Flyway 통합
# argocd/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp.git
path: helm
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ArgoCD Sync 흐름:
1. Git 변경 감지
2. Helm hook (pre-upgrade) → Flyway Job 실행
3. Flyway 마이그레이션 성공
4. 애플리케이션 Deployment 업데이트
5. ArgoCD 상태 동기화 완료
10. 베스트 프랙티스와 안티패턴
10.1 베스트 프랙티스
1. 하나의 마이그레이션 = 하나의 변경
좋은 예:
V1__Create_users_table.sql
V2__Add_email_index.sql
V3__Create_orders_table.sql
나쁜 예:
V1__Create_all_tables_and_indexes_and_data.sql
2. 적용된 마이그레이션은 절대 수정 금지
절대 하면 안 되는 것:
- 이미 실행된 V1__Create_users.sql 파일 수정
- Flyway는 체크섬으로 변경 감지 → 에러 발생
올바른 방법:
- 새 마이그레이션 파일로 변경 (V5__Fix_users_table.sql)
3. 프로덕션 유사 데이터로 테스트
# 프로덕션 데이터 규모를 모방한 테스트
# 1000만 건 테이블에서 ALTER TABLE 테스트
# 테스트 데이터 생성
INSERT INTO users_test (username, email)
SELECT
'user_' || generate_series,
'user_' || generate_series || '@example.com'
FROM generate_series(1, 10000000);
# 마이그레이션 실행 시간 측정
\timing on
ALTER TABLE users_test ADD COLUMN phone VARCHAR(20);
# Time: 45123.456 ms (약 45초) - 프로덕션에서 45초 잠금!
4. 스키마 변경과 데이터 변경 분리
V10__Add_status_column.sql # 스키마 변경
V11__Backfill_status_data.sql # 데이터 변경
V12__Add_status_not_null.sql # 제약 추가
5. 대규모 데이터 마이그레이션은 배치 처리
-- 나쁜 예: 한 번에 전체 업데이트
UPDATE users SET normalized_email = LOWER(email);
-- 좋은 예: 배치 처리
DO $block$
DECLARE
batch_size INT := 10000;
total_updated INT := 0;
rows_affected INT;
BEGIN
LOOP
UPDATE users
SET normalized_email = LOWER(email)
WHERE id IN (
SELECT id FROM users
WHERE normalized_email IS NULL
LIMIT batch_size
);
GET DIAGNOSTICS rows_affected = ROW_COUNT;
total_updated := total_updated + rows_affected;
RAISE NOTICE 'Updated % rows (total: %)', rows_affected, total_updated;
EXIT WHEN rows_affected = 0;
PERFORM pg_sleep(0.1);
END LOOP;
END;
$block$;
10.2 안티패턴
안티패턴 1: 프로덕션에서 직접 DDL 실행
→ 반드시 마이그레이션 파일을 통해 실행
안티패턴 2: 환경별 다른 마이그레이션 파일
→ 동일한 마이그레이션을 모든 환경에서 실행 (플레이스홀더 활용)
안티패턴 3: 마이그레이션에 비즈니스 로직 포함
→ 마이그레이션은 스키마/데이터 변경만. 로직은 애플리케이션에서
안티패턴 4: 하위 호환성 무시
→ 무중단 배포 시 이전 코드와 호환되어야 함
안티패턴 5: 테스트 없이 프로덕션 배포
→ CI에서 반드시 테스트 DB로 검증
안티패턴 6: 롤백 계획 없이 배포
→ 모든 마이그레이션에 롤백/보상 전략 준비
안티패턴 7: 마이그레이션 도구 버전 미고정
→ lock file이나 명시적 버전 지정
11. 프로덕션 배포 체크리스트
배포 전 반드시 확인해야 할 항목입니다.
사전 검토:
[ ] 마이그레이션 SQL 코드 리뷰 완료
[ ] 하위 호환성 확인 (이전 코드와 호환)
[ ] 프로덕션 유사 데이터로 테스트 완료
[ ] 실행 시간 측정 (대규모 테이블 주의)
[ ] 잠금 영향도 분석
[ ] 롤백/보상 마이그레이션 준비
배포 시:
[ ] DB 백업 완료 확인
[ ] 모니터링 대시보드 확인 (CPU, 연결 수, 느린 쿼리)
[ ] 유지보수 창 내 실행 (필요시)
[ ] Flyway validate 먼저 실행
[ ] 마이그레이션 실행 후 info 확인
[ ] 애플리케이션 헬스체크 확인
배포 후:
[ ] 스키마 변경 확인 (테이블, 인덱스, 제약)
[ ] 애플리케이션 정상 동작 확인
[ ] 성능 모니터링 (쿼리 성능 변화)
[ ] 에러 로그 확인
[ ] 팀 공유 (변경 내용, 영향도)
12. 퀴즈
Q1: Flyway 네이밍 컨벤션
Flyway에서 반복 실행(Repeatable) 마이그레이션 파일의 접두사는 무엇인가요?
정답: R__
Flyway 마이그레이션 파일 접두사는 다음과 같습니다.
V- Versioned Migration (한 번만 실행)R- Repeatable Migration (파일이 변경될 때마다 재실행)U- Undo Migration (Teams 에디션, 롤백용)
Repeatable 마이그레이션은 뷰, 스토어드 프로시저, 함수 등 매번 재생성이 필요한 객체에 사용됩니다.
Q2: Expand-Contract 패턴
무중단 스키마 변경에서 Expand-Contract 패턴의 3단계를 설명하세요.
정답:
-
Expand (확장): 새 컬럼이나 테이블을 추가합니다. 이전 코드와 새 코드가 모두 동작할 수 있도록 합니다. 새 컬럼은 nullable로 추가합니다.
-
Migrate (이전): 기존 데이터를 새 구조로 복사하거나 변환합니다. 이 단계에서 새 코드는 새 컬럼을 사용하도록 전환합니다.
-
Contract (축소): 모든 코드가 새 구조를 사용하는 것이 확인된 후, 이전 컬럼이나 테이블을 제거합니다.
핵심은 각 단계가 별도의 배포로 이루어진다는 것입니다. 하나의 배포에서 모두 처리하면 안 됩니다.
Q3: Prisma Shadow Database
Prisma Migrate에서 Shadow Database의 역할은 무엇인가요?
정답:
Shadow Database는 Prisma Migrate가 마이그레이션의 정확성을 검증하기 위해 사용하는 임시 데이터베이스입니다.
동작 과정:
- 임시 데이터베이스를 생성합니다
- 모든 기존 마이그레이션 파일을 순서대로 적용합니다
- 현재 Prisma 스키마와 비교하여 차이를 감지합니다
- 새 마이그레이션 SQL을 생성합니다
- 생성된 SQL을 Shadow DB에 적용하여 검증합니다
- 임시 데이터베이스를 삭제합니다
이를 통해 마이그레이션 드리프트를 감지하고, 잘못된 마이그레이션 파일을 방지할 수 있습니다.
Q4: CI/CD에서 마이그레이션 테스트
CI/CD 파이프라인에서 DB 마이그레이션을 안전하게 테스트하는 방법은?
정답:
-
테스트 데이터베이스 사용: CI 환경에서 Docker 등으로 임시 DB를 생성합니다.
-
Flyway validate 실행: 마이그레이션 파일의 유효성을 먼저 검증합니다.
-
전체 마이그레이션 적용: 빈 DB에서 모든 마이그레이션을 순서대로 실행합니다.
-
스키마 검증: 기대하는 테이블, 컬럼, 인덱스가 존재하는지 확인합니다.
-
프로덕션 유사 데이터로 성능 테스트: 대규모 데이터에서 마이그레이션 실행 시간과 잠금 영향을 측정합니다.
-
스테이징 환경에서 검증: 프로덕션 배포 전 스테이징에서 먼저 적용합니다.
-
승인 게이트: 프로덕션 배포 전 수동 승인 단계를 추가합니다.
Q5: 롤백 불가 시나리오
DB 마이그레이션에서 롤백하면 안 되는 상황 두 가지를 설명하세요.
정답:
-
컬럼 삭제 후 데이터 손실: DROP COLUMN으로 컬럼을 삭제한 마이그레이션은 롤백해도 데이터가 이미 사라져 복구할 수 없습니다. 반드시 백업을 먼저 확인해야 합니다.
-
비가역적 데이터 변환: 데이터를 변환(예: 이메일 해싱)한 후 원본 데이터를 보존하지 않은 경우, 롤백해도 원래 데이터를 복원할 수 없습니다.
추가:
- 대규모 데이터 이동 후 롤백 시간이 서비스 허용 범위를 초과하는 경우
- 외부 시스템과 연동된 변경으로 외부 시스템의 상태를 되돌릴 수 없는 경우
이런 상황에서는 Forward-Only 접근(보상 마이그레이션)이 더 안전합니다.