Migration
마이그레이션
데이터베이스 스키마 변경을 버전 관리하는 기술입니다. 업(Up) 마이그레이션으로 스키마를 변경하고, 다운(Down) 마이그레이션으로 롤백합니다. 팀 협업과 안전한 배포의 핵심입니다.
마이그레이션
데이터베이스 스키마 변경을 버전 관리하는 기술입니다. 업(Up) 마이그레이션으로 스키마를 변경하고, 다운(Down) 마이그레이션으로 롤백합니다. 팀 협업과 안전한 배포의 핵심입니다.
스키마 마이그레이션은 데이터베이스 구조(테이블, 컬럼, 인덱스 등)의 변경 이력을 코드로 관리하는 방법입니다. Git이 소스코드 버전을 관리하듯, 마이그레이션 도구는 DB 스키마 버전을 관리합니다.
Up 마이그레이션은 스키마를 새 버전으로 변경하는 작업입니다. 테이블 생성, 컬럼 추가, 인덱스 생성 등이 포함됩니다. Down 마이그레이션은 변경을 되돌리는 롤백 작업으로, 문제 발생 시 이전 상태로 복구할 수 있습니다.
마이그레이션 파일 명명 규칙: 일반적으로 타임스탬프 + 설명 형식을 사용합니다. 예: 20240115_143022_create_users_table.sql. 타임스탬프로 실행 순서를 보장하고, 설명으로 변경 내용을 파악합니다.
마이그레이션 테이블: 대부분의 도구는 적용된 마이그레이션을 추적하는 테이블(예: schema_migrations, flyway_schema_history)을 자동 생성합니다. 이를 통해 어떤 마이그레이션이 적용되었는지 확인하고, 중복 실행을 방지합니다.
주요 마이그레이션 도구: Flyway(Java), Alembic(Python), Prisma Migrate(Node.js), Knex.js, Django Migrations, Rails Active Record Migrations 등이 있습니다. 각 프레임워크와 언어에 맞는 도구를 선택하세요.
# Alembic 마이그레이션 예제 - Python + SQLAlchemy
# ==============================================
# 1. alembic.ini 설정 및 초기화
# ==============================================
# pip install alembic sqlalchemy
# alembic init migrations
# migrations/env.py - 데이터베이스 연결 설정
from alembic import context
from sqlalchemy import engine_from_config, pool
from myapp.models import Base # SQLAlchemy 모델
config = context.config
target_metadata = Base.metadata
def run_migrations_online():
connectable = engine_from_config(
config.get_section(config.config_ini_section),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata
)
with context.begin_transaction():
context.run_migrations()
# ==============================================
# 2. 마이그레이션 파일 생성
# ==============================================
# alembic revision -m "create users table"
# migrations/versions/20240115_create_users_table.py
"""create users table
Revision ID: a1b2c3d4e5f6
Revises:
Create Date: 2024-01-15 14:30:22.123456
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers
revision = 'a1b2c3d4e5f6'
down_revision = None # 첫 번째 마이그레이션
branch_labels = None
depends_on = None
def upgrade():
"""Up 마이그레이션 - 스키마 변경 적용"""
op.create_table(
'users',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('email', sa.String(255), nullable=False, unique=True),
sa.Column('username', sa.String(100), nullable=False),
sa.Column('password_hash', sa.String(255), nullable=False),
sa.Column('is_active', sa.Boolean(), default=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.func.now()),
sa.Column('updated_at', sa.DateTime(), onupdate=sa.func.now()),
)
# 인덱스 추가
op.create_index('ix_users_email', 'users', ['email'])
op.create_index('ix_users_username', 'users', ['username'])
def downgrade():
"""Down 마이그레이션 - 롤백"""
op.drop_index('ix_users_username', table_name='users')
op.drop_index('ix_users_email', table_name='users')
op.drop_table('users')
# ==============================================
# 3. 두 번째 마이그레이션 - 컬럼 추가
# ==============================================
# alembic revision -m "add profile columns to users"
"""add profile columns to users
Revision ID: b2c3d4e5f6g7
Revises: a1b2c3d4e5f6
"""
revision = 'b2c3d4e5f6g7'
down_revision = 'a1b2c3d4e5f6'
def upgrade():
# 컬럼 추가 (nullable=True로 시작)
op.add_column('users', sa.Column('avatar_url', sa.String(500), nullable=True))
op.add_column('users', sa.Column('bio', sa.Text(), nullable=True))
# 새 테이블 추가
op.create_table(
'user_profiles',
sa.Column('id', sa.Integer(), primary_key=True),
sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id'), unique=True),
sa.Column('phone', sa.String(20)),
sa.Column('address', sa.Text()),
)
def downgrade():
op.drop_table('user_profiles')
op.drop_column('users', 'bio')
op.drop_column('users', 'avatar_url')
# ==============================================
# 4. 마이그레이션 실행 명령어
# ==============================================
# 최신 버전으로 업그레이드
# alembic upgrade head
# 특정 버전으로 업그레이드
# alembic upgrade a1b2c3d4e5f6
# 한 단계 롤백
# alembic downgrade -1
# 특정 버전으로 롤백
# alembic downgrade a1b2c3d4e5f6
# 현재 버전 확인
# alembic current
# 마이그레이션 히스토리 확인
# alembic history
// Prisma 마이그레이션 예제 - Node.js / TypeScript
// ==============================================
// 1. Prisma 스키마 정의 (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
username String
passwordHash String @map("password_hash")
isActive Boolean @default(true) @map("is_active")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// 관계
posts Post[]
profile Profile?
@@map("users")
@@index([email])
@@index([username])
}
model Profile {
id Int @id @default(autoincrement())
userId Int @unique @map("user_id")
phone String?
address String?
avatarUrl String? @map("avatar_url")
bio String?
user User @relation(fields: [userId], references: [id])
@@map("user_profiles")
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int @map("author_id")
createdAt DateTime @default(now()) @map("created_at")
author User @relation(fields: [authorId], references: [id])
@@map("posts")
@@index([authorId])
}
*/
// ==============================================
// 2. 마이그레이션 명령어
// ==============================================
// 마이그레이션 생성 및 적용
// npx prisma migrate dev --name init
// 프로덕션에 마이그레이션 적용
// npx prisma migrate deploy
// 마이그레이션 상태 확인
// npx prisma migrate status
// 스키마 리셋 (개발 환경만!)
// npx prisma migrate reset
// ==============================================
// 3. 생성된 마이그레이션 파일 예시
// ==============================================
// prisma/migrations/20240115143022_init/migration.sql
/*
-- CreateTable
CREATE TABLE "users" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"username" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"is_active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "user_profiles" (
"id" SERIAL NOT NULL,
"user_id" INTEGER NOT NULL,
"phone" TEXT,
"address" TEXT,
"avatar_url" TEXT,
"bio" TEXT,
CONSTRAINT "user_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
CREATE INDEX "users_email_idx" ON "users"("email");
CREATE INDEX "users_username_idx" ON "users"("username");
-- CreateIndex
CREATE UNIQUE INDEX "user_profiles_user_id_key" ON "user_profiles"("user_id");
-- AddForeignKey
ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
*/
// ==============================================
// 4. 프로그래밍 방식 마이그레이션 (Knex.js)
// ==============================================
// knexfile.js
module.exports = {
development: {
client: 'postgresql',
connection: process.env.DATABASE_URL,
migrations: {
directory: './migrations',
tableName: 'knex_migrations'
}
}
};
// migrations/20240115143022_create_users.js
exports.up = function(knex) {
return knex.schema
.createTable('users', (table) => {
table.increments('id').primary();
table.string('email', 255).notNullable().unique();
table.string('username', 100).notNullable();
table.string('password_hash', 255).notNullable();
table.boolean('is_active').defaultTo(true);
table.timestamps(true, true); // created_at, updated_at
table.index('email');
table.index('username');
})
.createTable('user_profiles', (table) => {
table.increments('id').primary();
table.integer('user_id').unsigned().unique()
.references('id').inTable('users');
table.string('phone', 20);
table.text('address');
});
};
exports.down = function(knex) {
return knex.schema
.dropTableIfExists('user_profiles')
.dropTableIfExists('users');
};
// 실행: npx knex migrate:latest
// 롤백: npx knex migrate:rollback
-- ============================================
-- Flyway 마이그레이션 예제 (Raw SQL)
-- ============================================
-- 파일명: V1__create_users_table.sql
-- V{버전}__{설명}.sql 형식 (언더스코어 2개)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
username VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX ix_users_email ON users(email);
CREATE INDEX ix_users_username ON users(username);
-- 트리거: updated_at 자동 갱신
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- 파일명: V2__add_user_profiles.sql
-- ============================================
CREATE TABLE user_profiles (
id SERIAL PRIMARY KEY,
user_id INTEGER UNIQUE REFERENCES users(id) ON DELETE CASCADE,
phone VARCHAR(20),
address TEXT,
avatar_url VARCHAR(500),
bio TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- users 테이블에 컬럼 추가
ALTER TABLE users ADD COLUMN last_login_at TIMESTAMP;
ALTER TABLE users ADD COLUMN login_count INTEGER DEFAULT 0;
-- ============================================
-- 파일명: V3__create_posts_table.sql
-- ============================================
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
slug VARCHAR(255) UNIQUE,
published BOOLEAN DEFAULT FALSE,
author_id INTEGER NOT NULL REFERENCES users(id),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
published_at TIMESTAMP
);
CREATE INDEX ix_posts_author ON posts(author_id);
CREATE INDEX ix_posts_published ON posts(published) WHERE published = TRUE;
CREATE INDEX ix_posts_slug ON posts(slug);
-- ============================================
-- 무중단 마이그레이션 패턴 (대용량 테이블)
-- ============================================
-- V4__add_status_column_safe.sql
-- 1단계: nullable 컬럼 추가 (락 최소화)
ALTER TABLE posts ADD COLUMN status VARCHAR(20);
-- V5__backfill_status_column.sql
-- 2단계: 기존 데이터 백필 (배치로 처리)
UPDATE posts SET status = 'published' WHERE published = TRUE AND status IS NULL;
UPDATE posts SET status = 'draft' WHERE published = FALSE AND status IS NULL;
-- V6__add_status_constraints.sql
-- 3단계: NOT NULL 및 제약 조건 추가
ALTER TABLE posts ALTER COLUMN status SET DEFAULT 'draft';
ALTER TABLE posts ALTER COLUMN status SET NOT NULL;
ALTER TABLE posts ADD CONSTRAINT chk_posts_status
CHECK (status IN ('draft', 'published', 'archived'));
-- ============================================
-- Flyway 명령어
-- ============================================
-- 마이그레이션 적용
-- flyway -url=jdbc:postgresql://localhost/mydb -user=postgres migrate
-- 현재 상태 확인
-- flyway info
-- 마이그레이션 검증
-- flyway validate
-- 히스토리 정리 (주의!)
-- flyway repair
"이번 PR에 마이그레이션 파일 포함되어 있는데, down 마이그레이션도 테스트해봤나요? 프로덕션 배포 후 문제 생기면 롤백해야 하니까 down이 제대로 동작하는지 스테이징에서 검증하고 갑시다."
"users 테이블이 1억 건이라 ALTER TABLE로 NOT NULL 추가하면 락 걸려요. 3단계로 나눠서 가죠. 먼저 nullable로 컬럼 추가하고, 백그라운드로 백필하고, 마지막에 제약조건 추가하는 식으로요."
"로컬 DB 세팅은 간단해요. .env에 DATABASE_URL 설정하고 'prisma migrate dev' 한 번이면 끝이에요. 마이그레이션 히스토리가 Git에 있어서 팀 전체가 동일한 스키마를 유지할 수 있습니다."
이미 다른 환경에 적용된 마이그레이션을 수정하면 체크섬 불일치로 오류가 발생합니다. 수정이 필요하면 새 마이그레이션을 만드세요.
롤백 불가능한 마이그레이션은 장애 대응을 어렵게 합니다. 항상 down 마이그레이션을 작성하고 테스트하세요.
수억 건 테이블에 직접 ALTER TABLE 실행 시 장시간 락 발생합니다. pt-online-schema-change나 gh-ost 같은 무중단 도구를 사용하세요.
마이그레이션을 작게 분리, Git과 함께 버전 관리, CI/CD에서 자동 실행, 스테이징 먼저 적용 후 프로덕션, 데이터 마이그레이션은 별도 스크립트로.