Communitygithub.com

데이터베이스 마이그레이션

PostgreSQL, MySQL 및 일반적인 ORM(Prisma, Drizzle, Kysely, Django, TypeORM, golang-migrate)에서 스키마 변경, 데이터 마이그레이션, 롤백, 무중단 배포를 위한 데이터베이스 마이그레이션 모범 사례입니다. 스키마 또는 데이터 마이그레이션을 작성하거나, 롤백을 계획하거나, 무중단 배포를 목표로 할 때 사용하세요.

데이터베이스 마이그레이션란 무엇인가요?

이 스킬은 프로덕션 시스템에서 안전하고 되돌릴 수 있는 데이터베이스 스키마 변경을 위한 포괄적인 지침을 제공합니다. PostgreSQL 패턴, 여러 ORM(Prisma, Drizzle, Kysely, Django, golang-migrate)별 워크플로, 무중단 배포 전략을 다룹니다. 주요 원칙에는 모든 변경을 마이그레이션으로 취급, 프로덕션에서 순방향 전용 마이그레이션 사용, 스키마와 데이터 마이그레이션 분리, 프로덕션 크기의 데이터로 마이그레이션 테스트, 배포 후 마이그레이션 불변성 유지 등이 포함됩니다. 이 스킬은 동시 인덱스 생성, expand-contract 패턴을 통한 안전한 컬럼 이름 변경/삭제, 대규모 데이터 마이그레이션을 위한 배치 업데이트, 각 ORM에 특화된 마이그레이션 명령어와 코드 예제를 다루며, 안전 점검 목록과 일반적인 안티 패턴을 설명하여 견고하고 중단 없는 데이터베이스 변경을 지원합니다.

지원 대상~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/affaan-m/ECC/tree/main/skills/database-migrations

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

데이터베이스 마이그레이션 패턴

프로덕션 시스템을 위한 안전하고 되돌릴 수 있는 데이터베이스 스키마 변경.

활성화 시점

  • 데이터베이스 테이블 생성 또는 변경
  • 컬럼 또는 인덱스 추가/제거
  • 데이터 마이그레이션 실행 (백필, 변환)
  • 무중단 스키마 변경 계획
  • 새 프로젝트를 위한 마이그레이션 도구 설정

핵심 원칙

  1. 모든 변경은 마이그레이션이다 — 프로덕션 데이터베이스를 수동으로 변경하지 마세요.
  2. 프로덕션에서 마이그레이션은 순방향 전용이다 — 롤백은 새 순방향 마이그레이션을 사용합니다.
  3. 스키마와 데이터 마이그레이션은 분리한다 — 하나의 마이그레이션에서 DDL과 DML을 절대 혼용하지 마세요.
  4. 프로덕션 규모의 데이터로 마이그레이션을 테스트한다 — 100행에서 작동하는 마이그레이션이 1천만 행에서는 잠길 수 있습니다.
  5. 일단 배포된 마이그레이션은 불변이다 — 프로덕션에서 실행된 마이그레이션은 절대 편집하지 마세요.

마이그레이션 안전 점검 목록

마이그레이션을 적용하기 전에:

  • 마이그레이션에 UP과 DOWN이 모두 있거나 (또는 명시적으로 되돌릴 수 없음으로 표시)
  • 대형 테이블에 전체 테이블 잠금 없음 (동시 작업 사용)
  • 새 컬럼에 기본값이 있거나 NULL 허용 (기본값 없이 NOT NULL을 추가하지 마세요)
  • 인덱스가 동시에 생성됨 (기존 테이블의 경우 CREATE TABLE 인라인이 아님)
  • 데이터 백필은 스키마 변경과 별도의 마이그레이션
  • 프로덕션 데이터 복사본으로 테스트됨
  • 롤백 계획 문서화됨

PostgreSQL 패턴

안전하게 컬럼 추가하기

-- 좋음: NULL 허용, 잠금 없음
ALTER TABLE users ADD COLUMN avatar_url TEXT;

-- 좋음: 기본값이 있는 컬럼 (PostgreSQL 11+는 즉시 처리, 재작성 없음)
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT true;

-- 나쁨: 기존 테이블에 기본값 없는 NOT NULL (전체 재작성 필요)
ALTER TABLE users ADD COLUMN role TEXT NOT NULL;
-- 이 명령은 테이블을 잠그고 모든 행을 재작성합니다

다운타임 없이 인덱스 추가하기

-- 나쁨: 대형 테이블에서 쓰기 차단
CREATE INDEX idx_users_email ON users (email);

-- 좋음: 비차단, 동시 쓰기 허용
CREATE INDEX CONCURRENTLY idx_users_email ON users (email);

-- 참고: CONCURRENTLY는 트랜잭션 블록 내에서 실행할 수 없습니다.
-- 대부분의 마이그레이션 도구는 이를 위해 특별한 처리가 필요합니다.

컬럼 이름 바꾸기 (무중단)

프로덕션에서 직접 이름을 바꾸지 마세요. 확장-축소 패턴을 사용하세요:

-- 단계 1: 새 컬럼 추가 (마이그레이션 001)
ALTER TABLE users ADD COLUMN display_name TEXT;

-- 단계 2: 데이터 백필 (마이그레이션 002, 데이터 마이그레이션)
UPDATE users SET display_name = username WHERE display_name IS NULL;

-- 단계 3: 애플리케이션 코드를 업데이트하여 두 컬럼 읽기/쓰기
-- 애플리케이션 변경 사항 배포

-- 단계 4: 이전 컬럼에 쓰기 중단, 삭제 (마이그레이션 003)
ALTER TABLE users DROP COLUMN username;

안전하게 컬럼 제거하기

-- 단계 1: 컬럼에 대한 모든 애플리케이션 참조 제거
-- 단계 2: 컬럼 참조 없이 애플리케이션 배포
-- 단계 3: 다음 마이그레이션에서 컬럼 삭제
ALTER TABLE orders DROP COLUMN legacy_status;

-- Django의 경우: SeparateDatabaseAndState를 사용하여 모델에서 제거
-- DROP COLUMN을 생성하지 않고 (다음 마이그레이션에서 삭제)

대규모 데이터 마이그레이션

-- 나쁨: 하나의 트랜잭션에서 모든 행 업데이트 (테이블 잠금)
UPDATE users SET normalized_email = LOWER(email);

-- 좋음: 진행 상황과 함께 배치 업데이트
DO $$
DECLARE
  batch_size INT := 10000;
  rows_updated 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
      FOR UPDATE SKIP LOCKED
    );
    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    RAISE NOTICE 'Updated % rows', rows_updated;
    EXIT WHEN rows_updated = 0;
    COMMIT;
  END LOOP;
END $$;

Prisma (TypeScript/Node.js)

워크플로

# 스키마 변경 사항에서 마이그레이션 생성
npx prisma migrate dev --name add_user_avatar

# 프로덕션에서 보류 중인 마이그레이션 적용
npx prisma migrate deploy

# 데이터베이스 초기화 (개발 전용)
 npx prisma migrate reset

# 스키마 변경 후 클라이언트 생성
npx prisma generate

스키마 예제

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  avatarUrl String?  @map("avatar_url")
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")
  orders    Order[]

  @@map("users")
  @@index([email])
}

커스텀 SQL 마이그레이션

Prisma가 표현할 수 없는 작업(동시 인덱스, 데이터 백필)의 경우:

# 빈 마이그레이션 생성 후 SQL 수동 편집
npx prisma migrate dev --create-only --name add_email_index
-- migrations/20240115_add_email_index/migration.sql
-- Prisma는 CONCURRENTLY를 생성할 수 없으므로 수동으로 작성합니다.
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_email ON users (email);

Drizzle (TypeScript/Node.js)

워크플로

# 스키마 변경 사항에서 마이그레이션 생성
npx drizzle-kit generate

# 마이그레이션 적용
npx drizzle-kit migrate

# 스키마 직접 푸시 (개발 전용, 마이그레이션 파일 없음)
npx drizzle-kit push

스키마 예제

import { pgTable, text, timestamp, uuid, boolean } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: uuid("id").primaryKey().defaultRandom(),
  email: text("email").notNull().unique(),
  name: text("name"),
  isActive: boolean("is_active").notNull().default(true),
  createdAt: timestamp("created_at").notNull().defaultNow(),
  updatedAt: timestamp("updated_at").notNull().defaultNow(),
});

Kysely (TypeScript/Node.js)

워크플로 (kysely-ctl)

# 설정 파일 초기화 (kysely.config.ts)
kysely init

# 새 마이그레이션 파일 생성
kysely migrate make add_user_avatar

# 보류 중인 모든 마이그레이션 적용
kysely migrate latest

# 마지막 마이그레이션 롤백
kysely migrate down

# 마이그레이션 상태 보기
kysely migrate list

마이그레이션 파일

// migrations/2024_01_15_001_create_user_profile.ts
import { type Kysely, sql } from 'kysely'

// 중요: 항상 Kysely의 <any>를 사용하고, 타입이 지정된 DB 인터페이스를 사용하지 마세요.
// 마이그레이션은 시간에 고정되어 있으며 현재 스키마 타입에 의존해서는 안 됩니다.
export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('user_profile')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
    .addColumn('avatar_url', 'text')
    .addColumn('created_at', 'timestamp', (col) =>
      col.defaultTo(sql`now()`).notNull()
    )
    .execute()

  await db.schema
    .createIndex('idx_user_profile_avatar')
    .on('user_profile')
    .column('avatar_url')
    .execute()
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('user_profile').execute()
}

프로그래매틱 마이그레이터

import { Migrator, FileMigrationProvider } from 'kysely'
import { promises as fs } from 'fs'
import * as path from 'path'
// ESM 전용 — CJS는 __dirname을 직접 사용할 수 있습니다.
import { fileURLToPath } from 'url'
const migrationFolder = path.join(
  path.dirname(fileURLToPath(import.meta.url)),
  './migrations',
)

// `db`는 Kysely<any> 데이터베이스 인스턴스입니다.
const migrator = new Migrator({
  db,
  provider: new FileMigrationProvider({
    fs,
    path,
    migrationFolder,
  }),
  // 경고: 개발 환경에서만 활성화하세요. 타임스탬프 순서 유효성 검사를 비활성화하여
  // 환경 간 스키마 드리프트를 유발할 수 있습니다.
  // allowUnorderedMigrations: true,
})

const { error, results } = await migrator.migrateToLatest()

results?.forEach((it) => {
  if (it.status === 'Success') {
    console.log(`migration "${it.migrationName}" executed successfully`)
  } else if (it.status === 'Error') {
    console.error(`failed to execute migration "${it.migrationName}"`)
  }
})

if (error) {
  console.error('migration failed', error)
  process.exit(1)
}

Django (Python)

워크플로

# 모델 변경 사항에서 마이그레이션 생성
python manage.py makemigrations

# 마이그레이션 적용
python manage.py migrate

# 마이그레이션 상태 보기
python manage.py showmigrations

# 커스텀 SQL을 위한 빈 마이그레이션 생성
python manage.py makemigrations --empty app_name -n description

데이터 마이그레이션

from django.db import migrations

def backfill_display_names(apps, schema_editor):
    User = apps.get_model("accounts", "User")
    batch_size = 5000
    users = User.objects.filter(display_name="")
    while users.exists():
        batch = list(users[:batch_size])
        for user in batch:
            user.display_name = user.username
        User.objects.bulk_update(batch, ["display_name"], batch_size=batch_size)

def reverse_backfill(apps, schema_editor):
    pass  # 데이터 마이그레이션, 롤백 불필요

class Migration(migrations.Migration):
    dependencies = [("accounts", "0015_add_display_name")]

    operations = [
        migrations.RunPython(backfill_display_names, reverse_backfill),
    ]

SeparateDatabaseAndState

데이터베이스에서 즉시 컬럼을 삭제하지 않고 Django 모델에서 컬럼 제거:

class Migration(migrations.Migration):
    operations = [
        migrations.SeparateDatabaseAndState(
            state_operations=[
                migrations.RemoveField(model_name="user", name="legacy_field"),
            ],
            database_operations=[],  # 아직 DB를 건드리지 않음
        ),
    ]

golang-migrate (Go)

워크플로

# 마이그레이션 쌍 생성
migrate create -ext sql -dir migrations -seq add_user_avatar

# 보류 중인 모든 마이그레이션 적용
migrate -path migrations -database "$DATABASE_URL" up

# 마지막 마이그레이션 롤백
migrate -path migrations -database "$DATABASE_URL" down 1

# 버전 강제 설정 (더티 상태 수정)
migrate -path migrations -database "$DATABASE_URL" force VERSION

마이그레이션 파일

-- migrations/000003_add_user_avatar.up.sql
ALTER TABLE users ADD COLUMN avatar_url TEXT;
CREATE INDEX CONCURRENTLY idx_users_avatar ON users (avatar_url) WHERE avatar_url IS NOT NULL;

-- migrations/000003_add_user_avatar.down.sql
DROP INDEX IF EXISTS idx_users_avatar;
ALTER TABLE users DROP COLUMN IF EXISTS avatar_url;

무중단 마이그레이션 전략

중요한 프로덕션 변경의 경우 확장-축소 패턴을 따르세요:

단계 1: 확장
  - 새 컬럼/테이블 추가 (NULL 허용 또는 기본값 포함)
  - 배포: 앱이 이전 컬럼과 새 컬럼 모두에 쓰기
  - 기존 데이터 백필

단계 2: 마이그레이션
  - 배포: 앱이 새 컬럼에서 읽고, 두 컬럼 모두에 쓰기
  - 데이터 정합성 확인

단계 3: 축소
  - 배포: 앱이 새 컬럼만 사용
  - 별도 마이그레이션에서 이전 컬럼/테이블 삭제

타임라인 예제

1일차: 마이그레이션이 new_status 컬럼 추가 (NULL 허용)
1일차: 앱 v2 배포 — status와 new_status 모두에 쓰기
2일차: 기존 행에 대한 백필 마이그레이션 실행
3일차: 앱 v3 배포 — new_status에서만 읽기
7일차: 마이그레이션이 이전 status 컬럼 삭제

안티 패턴

안티 패턴실패 이유더 나은 접근법
프로덕션에서 수동 SQL감사 추적 없음, 반복 불가항상 마이그레이션 파일 사용
배포된 마이그레이션 편집환경 간 드리프트 발생대신 새 마이그레이션 생성
기본값 없는 NOT NULL테이블 잠금, 모든 행 재작성NULL 허용 추가, 백필, 제약 조건 추가
대형 테이블에 인라인 인덱스빌드 중 쓰기 차단CREATE INDEX CONCURRENTLY
하나의 마이그레이션에 스키마 + 데이터롤백 어려움, 긴 트랜잭션별도 마이그레이션
코드 제거 전 컬럼 삭제누락된 컬럼으로 애플리케이션 오류먼저 코드 제거, 다음 배포에서 컬럼 삭제

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

accessibility

Design, implement, and audit inclusive digital products using WCAG 2.2 Level AA. Use when building or auditing UI that must meet WCAG 2.2 Level AA, or when reviewing a change for keyboard, contrast, or screen-reader support.

affaan-m/content-engine

Create platform-native content systems for X, LinkedIn, TikTok, YouTube, newsletters, and repurposed multi-platform campaigns. Use when the user wants social posts, threads, scripts, content calendars, or one source asset adapted cleanly across platforms.

affaan-m/ECC

Multi-platform content distribution across X, LinkedIn, Threads, and Bluesky. Adapts content per platform using content-engine patterns. Never posts identical content cross-platform. Use when the user wants to distribute content across social platforms.

affaan-m/fal-ai-media

Unified media generation via fal.ai MCP — image, video, and audio. Covers text-to-image (Nano Banana), text/image-to-video (Seedance, Kling, Veo 3), text-to-speech (CSM-1B), and video-to-audio (ThinkSound). Use when the user wants to generate images, videos, or audio with AI.

affaan-m/manim-video

日本語翻訳:このファイルは manim-video 用の日本語翻訳が必要です

affaan-m/remotion-video-creation

Remotion のベストプラクティス - React で動画を作成する。3D、アニメーション、音声、字幕、チャート、トランジションなどをカバーするドメイン固有の29のルール。

affaan-m/video-editing

AI-assisted video editing workflows for cutting, structuring, and augmenting real footage. Covers the full pipeline from raw capture through FFmpeg, Remotion, ElevenLabs, fal.ai, and final polish in Descript or CapCut. Use when the user wants to edit video, cut footage, create vlogs, or build video content.

agent-architecture-audit

Full-stack diagnostic for agent and LLM applications. Audits the 12-layer agent stack for wrapper regression, memory pollution, tool discipline failures, hidden repair loops, and rendering corruption. Produces severity-ranked findings with code-first fixes. Essential for developers building agent applications, autonomous loops, or any LLM-powered feature. Use when an agent or LLM feature misbehaves and the failing layer is unknown, or before shipping an agent stack.

agent-eval

Head-to-head comparison of coding agents (Claude Code, Aider, Codex, etc.) on custom tasks with pass rate, cost, time, and consistency metrics. Use when choosing between coding agents, or when a change to an agent setup needs measured pass rate, cost, and time rather than an impression.

agent-harness-construction

Design and optimize AI agent action spaces, tool definitions, and observation formatting for higher completion rates. Use when defining or revising an agent

agentic-engineering

Operate as an agentic engineer using eval-first execution, decomposition, and cost-aware model routing. Use when planning or executing engineering work that agents will carry out end to end.

agentic-os

Build persistent multi-agent operating systems on Claude Code. Covers kernel architecture, specialist agents, slash commands, file-based memory, scheduled automation, and state management without external databases. Use when building a persistent multi-agent system on Claude Code with its own memory, commands, and scheduling.

agent-introspection-debugging

Structured self-debugging workflow for AI agent failures using capture, diagnosis, contained recovery, and introspection reports. Use when an agent run fails and you need a reproducible diagnosis instead of a retry.

agent-payment-x402

Add x402 payment execution to AI agents with per-task budgets, spending controls, and non-custodial wallets. Supports Base through agentwallet-sdk and X Layer through OKX Payments / OKX Agent Payments Protocol. Use when an agent must pay for something itself and needs per-task budgets, spending controls, and a non-custodial wallet.

agent-self-evaluation

Use after completing any non-trivial task. The agent self-rates its output on 5 axes — accuracy, completeness, clarity, actionability, conciseness — with concrete evidence per criterion. Produces a structured 1-5 scorecard with specific improvement suggestions.

agent-sort

Build an evidence-backed ECC install plan for a specific repo by sorting skills, commands, rules, hooks, and extras into DAILY vs LIBRARY buckets using parallel repo-aware review passes. Use when ECC should be trimmed to what a project actually needs instead of loading the full bundle.

ai-first-engineering

Engineering operating model for teams where AI agents generate a large share of implementation output. Use when setting team process, review gates, or ownership rules for a codebase largely written by agents.

ai-regression-testing

Regression testing strategies for AI-assisted development. Sandbox-mode API testing without database dependencies, automated bug-check workflows, and patterns to catch AI blind spots where the same model writes and reviews code. Use when adding regression coverage to AI-assisted code, or when the same model both wrote and reviewed a change.

android-clean-architecture

Clean Architecture patterns for Android and Kotlin Multiplatform projects — module structure, dependency rules, UseCases, Repositories, and data layer patterns. Use when structuring modules, layers, or data flow in an Android or KMP project.

angular-developer

Generates Angular code and provides architectural guidance. Trigger when creating projects, components, or services, or for best practices on reactivity (signals, linkedSignal, resource), forms, dependency injection, routing, SSR, accessibility (ARIA), animations, styling (component styles, Tailwind CSS), testing, or CLI tooling.

관련 스킬