Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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

핵심 원칙은 다음과 같습니다.

1. **모든 스키마 변경은 마이그레이션 파일로** - 직접 SQL 실행 금지

2. **마이그레이션은 코드와 함께 버전 관리** - Git 저장소에 포함

3. **자동화된 실행** - CI/CD 파이프라인에서 실행

4. **멱등성(Idempotency)** - 같은 마이그레이션을 여러 번 실행해도 동일 결과

5. **순서 보장** - 마이그레이션은 정해진 순서대로 실행

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 플러그인**

**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;

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

Entity, PrimaryGeneratedColumn, Column,

CreateDateColumn, UpdateDateColumn, OneToMany

} from 'typeorm';

@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

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

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 네이밍 컨벤션

**정답: R__**

Flyway 마이그레이션 파일 접두사는 다음과 같습니다.

- `V` - Versioned Migration (한 번만 실행)

- `R` - Repeatable Migration (파일이 변경될 때마다 재실행)

- `U` - Undo Migration (Teams 에디션, 롤백용)

Repeatable 마이그레이션은 뷰, 스토어드 프로시저, 함수 등 매번 재생성이 필요한 객체에 사용됩니다.

Q2: Expand-Contract 패턴

**정답:**

1. **Expand (확장)**: 새 컬럼이나 테이블을 추가합니다. 이전 코드와 새 코드가 모두 동작할 수 있도록 합니다. 새 컬럼은 nullable로 추가합니다.

2. **Migrate (이전)**: 기존 데이터를 새 구조로 복사하거나 변환합니다. 이 단계에서 새 코드는 새 컬럼을 사용하도록 전환합니다.

3. **Contract (축소)**: 모든 코드가 새 구조를 사용하는 것이 확인된 후, 이전 컬럼이나 테이블을 제거합니다.

핵심은 각 단계가 별도의 배포로 이루어진다는 것입니다. 하나의 배포에서 모두 처리하면 안 됩니다.

Q3: Prisma Shadow Database

**정답:**

Shadow Database는 Prisma Migrate가 마이그레이션의 정확성을 검증하기 위해 사용하는 임시 데이터베이스입니다.

동작 과정:

1. 임시 데이터베이스를 생성합니다

2. 모든 기존 마이그레이션 파일을 순서대로 적용합니다

3. 현재 Prisma 스키마와 비교하여 차이를 감지합니다

4. 새 마이그레이션 SQL을 생성합니다

5. 생성된 SQL을 Shadow DB에 적용하여 검증합니다

6. 임시 데이터베이스를 삭제합니다

이를 통해 마이그레이션 드리프트를 감지하고, 잘못된 마이그레이션 파일을 방지할 수 있습니다.

Q4: CI/CD에서 마이그레이션 테스트

**정답:**

1. **테스트 데이터베이스 사용**: CI 환경에서 Docker 등으로 임시 DB를 생성합니다.

2. **Flyway validate 실행**: 마이그레이션 파일의 유효성을 먼저 검증합니다.

3. **전체 마이그레이션 적용**: 빈 DB에서 모든 마이그레이션을 순서대로 실행합니다.

4. **스키마 검증**: 기대하는 테이블, 컬럼, 인덱스가 존재하는지 확인합니다.

5. **프로덕션 유사 데이터로 성능 테스트**: 대규모 데이터에서 마이그레이션 실행 시간과 잠금 영향을 측정합니다.

6. **스테이징 환경에서 검증**: 프로덕션 배포 전 스테이징에서 먼저 적용합니다.

7. **승인 게이트**: 프로덕션 배포 전 수동 승인 단계를 추가합니다.

Q5: 롤백 불가 시나리오

**정답:**

1. **컬럼 삭제 후 데이터 손실**: DROP COLUMN으로 컬럼을 삭제한 마이그레이션은 롤백해도 데이터가 이미 사라져 복구할 수 없습니다. 반드시 백업을 먼저 확인해야 합니다.

2. **비가역적 데이터 변환**: 데이터를 변환(예: 이메일 해싱)한 후 원본 데이터를 보존하지 않은 경우, 롤백해도 원래 데이터를 복원할 수 없습니다.

추가:

- 대규모 데이터 이동 후 롤백 시간이 서비스 허용 범위를 초과하는 경우

- 외부 시스템과 연동된 변경으로 외부 시스템의 상태를 되돌릴 수 없는 경우

이런 상황에서는 Forward-Only 접근(보상 마이그레이션)이 더 안전합니다.

13. 참고 자료

- [Flyway 공식 문서](https://documentation.red-gate.com/fd)

- [Flyway GitHub](https://github.com/flyway/flyway)

- [Liquibase 공식 문서](https://docs.liquibase.com/)

- [Prisma Migrate 문서](https://www.prisma.io/docs/concepts/components/prisma-migrate)

- [TypeORM Migrations](https://typeorm.io/migrations)

- [Alembic 공식 문서](https://alembic.sqlalchemy.org/)

- [Django Migrations 공식 문서](https://docs.djangoproject.com/en/5.0/topics/migrations/)

- [GitHub gh-ost](https://github.com/github/gh-ost)

- [Percona pt-online-schema-change](https://docs.percona.com/percona-toolkit/pt-online-schema-change.html)

- [PostgreSQL ALTER TABLE 모범 사례](https://www.postgresql.org/docs/current/sql-altertable.html)

- [Expand-Contract 패턴](https://martinfowler.com/bliki/ParallelChange.html)

- [Kubernetes Init Containers](https://kubernetes.io/docs/concepts/workloads/pods/init-containers/)

- [Flyway와 Spring Boot 통합](https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-initialization.migration-tool.flyway)

현재 단락 (1/1164)

코드는 Git으로 완벽하게 버전 관리하면서, 데이터베이스 스키마는 수동으로 ALTER TABLE을 실행하고 있다면 심각한 문제가 존재합니다.

작성 글자: 0원문 글자: 30,309작성 단락: 0/1164