Claude Code로 프로덕션 DB 마이그레이션 안전하게 하기
Claude Code로 expand/contract, Prisma, CI, 백필, 롤백 한계를 포함한 프로덕션 DB 마이그레이션을 설계합니다.
프로덕션 데이터베이스 마이그레이션은 Claude Code에게 “스키마 바꿔줘”라고 맡길 작업이 아닙니다. 실제 위험은 SQL 문법보다 순서에 있습니다. 애플리케이션 배포, DB lock, 백업, 데이터 백필, feature flag, CI 검사, 롤백 판단이 서로 연결되어 있기 때문입니다.
이 글은 PostgreSQL과 Prisma Migrate를 쓰는 팀을 기준으로, Claude Code를 안전하게 활용하는 방법을 정리합니다. 순수 SQL migration을 쓰는 팀도 같은 모델을 적용할 수 있습니다. 핵심은 먼저 DB가 기존 앱과 새 앱을 모두 받아들이게 만들고, 데이터를 작게 옮긴 뒤, 검증이 끝난 후에 오래된 경로를 제거하는 것입니다.
리뷰할 때는 공식 문서를 같이 봐야 합니다. Claude Code는 Anthropic 공식 문서, PostgreSQL lock과 DDL은 explicit locking 및 ALTER TABLE, Prisma 운영은 development and production과 CLI reference, CI는 GitHub Actions workflow syntax를 확인하세요.
기본 모델
프로덕션 마이그레이션의 기본은 expand/contract입니다. expand는 DB를 넓히는 단계입니다. 예전 앱 버전과 새 앱 버전이 동시에 동작할 수 있도록 nullable 컬럼이나 새 테이블을 먼저 추가합니다. contract는 새 버전이 안정화되고 백필 검증이 끝난 뒤, 오래된 컬럼이나 읽기 경로를 제거하는 단계입니다.
flowchart LR
A["Backup and review"]
B["Expand: add nullable column or new table"]
C["Deploy code with dual write or feature flag"]
D["Backfill data in small batches"]
E["Validate staging and production metrics"]
F["Contract: add NOT NULL, remove old path"]
A --> B --> C --> D --> E --> F
흔한 실패는 Claude Code가 컬럼 추가, 데이터 복사, NOT NULL 설정, 기존 컬럼 삭제를 하나의 migration에 넣는 것입니다. 작은 로컬 DB에서는 통과할 수 있지만, 수백만 행의 users나 orders 테이블에서는 긴 lock 대기, 쓰기 중단, 타임아웃, 복구하기 어려운 데이터 손실로 이어질 수 있습니다.
용어도 분명히 해야 합니다. lock은 같은 테이블을 동시에 위험하게 변경하지 못하게 하는 DB의 대기 장치입니다. 백필은 기존 행에 새 컬럼 값을 채우는 작업입니다. shadow database는 Prisma가 개발 환경에서 migration history를 재생하고 drift를 찾기 위해 쓰는 임시 DB입니다. 프로덕션에서 위험을 자동으로 없애주는 기능은 아닙니다.
먼저 리뷰를 요청하기
첫 prompt는 구현 요청이 아니라 리뷰 요청이어야 합니다. 테이블 크기, 배포 방식, ORM, 복구 기대치를 Claude Code에게 알려줍니다.
Review this database migration plan before editing files.
Context:
- Production database: PostgreSQL
- ORM: Prisma Migrate
- Hot tables: users has about 8 million rows, orders has about 25 million rows
- Deploy style: blue/green app deploy, database migration runs in CI/CD
- Requirement: split users.name into users.full_name and users.display_name
Check:
1. Can old and new app versions run at the same time?
2. Which SQL statements may take strong locks or scan the whole table?
3. Which steps must be expand, backfill, validate, and contract?
4. What backup or point-in-time recovery check is needed before deploy?
5. What can be rolled back by app deploy, and what can only be rolled forward?
Return a migration plan first. Do not edit files yet.
마지막 문장이 중요합니다. Claude Code는 빠르게 파일을 수정할 수 있지만, DB 작업에서는 한 번 멈춰서 계획을 검토해야 합니다. 계획이 위험한 단계를 섞고 있다면 다시 지시합니다.
Rewrite the plan so that no step drops a column, rewrites a large table, or sets NOT NULL before the backfill is verified. Include a staging rehearsal and a production abort condition.
이 방식이면 Claude Code는 migration reviewer 역할을 합니다. 사람은 리스크를 판단하고, Claude Code는 관련 파일, 명령, SQL, CI 검사, 모니터링 항목을 빠짐없이 정리합니다.
Expand SQL 예시
users.name을 full_name과 display_name으로 나누는 예를 보겠습니다. expand 단계에서는 새 nullable 컬럼과 인덱스만 추가합니다. 백필, NOT NULL, 기존 컬럼 삭제는 하지 않습니다.
-- 20260602090000_expand_users_names.sql
-- Keep this migration small. Do not backfill and do not drop users.name here.
ALTER TABLE users
ADD COLUMN full_name text,
ADD COLUMN display_name text;
-- Run outside a transaction in PostgreSQL migration tools that support it.
-- CREATE INDEX CONCURRENTLY cannot run inside a transaction block.
CREATE INDEX CONCURRENTLY IF NOT EXISTS users_display_name_idx
ON users (display_name);
PostgreSQL의 ALTER TABLE은 하위 명령에 따라 lock 수준이 달라집니다. 문서에 더 약한 lock이 명시되지 않은 경우 보수적으로 판단해야 합니다. Claude Code에게도 “lock 수준을 추측하지 말고 공식 문서를 기준으로 검토하라”고 시켜야 합니다.
Prisma를 사용한다면 migration을 바로 적용하지 말고 먼저 생성만 합니다.
npx prisma migrate dev --name expand-users-names --create-only
npx prisma validate
Prisma 공식 가이드는 테스트와 프로덕션 환경에서 npx prisma migrate deploy를 사용한다고 설명합니다. 동시에 이 명령은 pending migration을 적용하지만 drift를 감지하지 않고 shadow database에도 의존하지 않는다고 설명합니다. 그래서 deploy가 성공했다는 사실만으로 운영 검증이 끝난 것은 아닙니다.
npx prisma migrate deploy
애플리케이션 전환
expand 이후 앱은 기존 스키마와 새 스키마를 모두 견뎌야 합니다. 읽기는 fallback을 가져야 하고, 쓰기는 전환 기간 동안 기존 필드와 새 필드를 함께 채우는 것이 안전합니다.
// src/domain/userName.ts
type UserNameRow = {
name: string | null;
fullName: string | null;
displayName: string | null;
};
export function readDisplayName(user: UserNameRow): string {
return user.displayName ?? user.fullName ?? user.name ?? "Unknown user";
}
export function buildNameUpdate(input: { name: string }) {
const normalized = input.name.trim().replace(/\s+/g, " ");
return {
name: normalized,
fullName: normalized,
displayName: normalized.length > 40 ? `${normalized.slice(0, 39)}...` : normalized,
};
}
feature flag를 쓰면 DB 변경과 사용자에게 보이는 동작을 분리할 수 있습니다. DB는 두 경로를 모두 받도록 만들고, 백필 검증 후 새 읽기 경로를 켭니다. 문제가 생기면 DB를 되돌리는 대신 flag를 끄거나 앱 버전을 되돌릴 수 있습니다. 구현은 Claude Code feature flags 가이드를 함께 보세요.
이 패턴은 세 가지 이상의 실제 사례에 적용됩니다. 사용자 이름, 주소, 전화번호 같은 프로필 필드 정규화, 주문 합계나 청구 잔액 같은 계산 컬럼 추가, 트래픽이 큰 테이블에 인덱스나 외래 키를 나중에 붙이는 작업입니다.
백필은 작은 배치로
하나의 거대한 UPDATE로 전체 백필을 끝내려 하면 lock 시간, WAL, replica lag, 장애 가능성이 커집니다. Claude Code에게 재시작 가능한 배치 스크립트를 만들게 하는 편이 낫습니다.
// scripts/backfill-user-names.mjs
import pg from "pg";
const { Client } = pg;
const batchSize = Number(process.env.BATCH_SIZE ?? 1000);
const sleepMs = Number(process.env.SLEEP_MS ?? 200);
const client = new Client({ connectionString: process.env.DATABASE_URL });
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
await client.connect();
try {
let total = 0;
while (true) {
const result = await client.query(
`
WITH target AS (
SELECT id, name
FROM users
WHERE full_name IS NULL
AND name IS NOT NULL
ORDER BY id
LIMIT $1
FOR UPDATE SKIP LOCKED
)
UPDATE users AS u
SET
full_name = target.name,
display_name = CASE
WHEN length(target.name) > 40 THEN substring(target.name from 1 for 39) || '...'
ELSE target.name
END
FROM target
WHERE u.id = target.id
RETURNING u.id
`,
[batchSize],
);
total += result.rowCount;
console.log(`updated=${result.rowCount} total=${total}`);
if (result.rowCount === 0) break;
await sleep(sleepMs);
}
} finally {
await client.end();
}
리뷰할 때는 네 가지를 확인합니다. 스크립트가 idempotent한지, 중간 실패 후 다시 실행 가능한지, 여러 프로세스가 동시에 실행되어도 안전한지, 운영 중 멈추고 관찰할 수 있는지입니다. 좋은 백필에는 batch size, sleep, 로그, 중단 기준, 반복 실행 가능한 쿼리가 있습니다.
CI와 Staging
CI에서는 임시 PostgreSQL에 migration history를 처음부터 적용해야 합니다. GitHub Actions에서는 workflow를 .github/workflows 아래 YAML 파일로 관리합니다.
name: migration-check
on:
pull_request:
paths:
- "prisma/**"
- "scripts/backfill-*.mjs"
- ".github/workflows/migration-check.yml"
jobs:
prisma-migrations:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app
ports:
- "5432:5432"
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/app?schema=public
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npx prisma validate
- run: npx prisma migrate deploy
- run: npx prisma migrate status
- name: Detect schema drift after migrations
run: |
npx prisma migrate diff \
--exit-code \
--from-config-datasource \
--to-schema=prisma/schema.prisma
이 예시는 Prisma ORM v7 이후의 config datasource 인자를 사용합니다. 오래된 예시에 있는 --from-url 또는 --shadow-database-url을 복사하기 전에 현재 CLI reference를 확인해야 합니다.
staging은 CI보다 현실에 가까워야 합니다. 비슷한 행 수, 인덱스, timeout, migration runner를 사용하고, lock wait, replica lag, 쿼리 latency, 앱 오류 로그, abort threshold를 확인합니다. 이 목록을 Claude Code에게 체크리스트로 만들게 하면 리뷰가 쉬워집니다.
Contract와 롤백 한계
새 앱이 안정화되고 백필이 검증된 뒤에만 contract를 진행합니다. NOT NULL도 바로 걸지 말고 검증용 제약으로 먼저 데이터 구멍을 확인합니다.
-- 20260602120000_contract_users_names.sql
-- Run only after the new application version has been stable in production.
ALTER TABLE users
ADD CONSTRAINT users_full_name_present
CHECK (full_name IS NOT NULL) NOT VALID;
ALTER TABLE users
VALIDATE CONSTRAINT users_full_name_present;
ALTER TABLE users
ALTER COLUMN full_name SET NOT NULL;
ALTER TABLE users
DROP CONSTRAINT users_full_name_present;
-- Drop old columns in a later deploy, not in the same deploy that changes reads.
-- ALTER TABLE users DROP COLUMN name;
가장 위험한 착각은 down migration이 모든 것을 되돌린다고 믿는 것입니다. 삭제된 컬럼, 덮어쓴 값, 손실이 있는 타입 변환은 SQL을 반대로 실행해도 원래대로 돌아오지 않습니다. 실제로 되돌릴 수 있는 것은 앱 버전이나 feature flag인 경우가 많고, DB는 백업, point-in-time recovery, 또는 forward fix로 복구해야 합니다.
Prisma의 migrate resolve --rolled-back도 성공한 migration을 되돌리는 명령이 아닙니다. 실패한 migration history 상태를 정리하는 명령입니다. Claude Code에게 롤백 계획을 쓰게 할 때는 “앱 롤백”, “DB forward fix”, “데이터 복구 필요”를 분리하게 하세요.
자주 발생하는 실패와 팀 운영
첫째, rename을 drop and add로 생성해서 데이터를 잃는 경우입니다. ORM은 의도를 완벽하게 알지 못하므로 생성 SQL을 반드시 봐야 합니다. 둘째, schema 변경과 대량 데이터 갱신을 하나의 migration에 넣는 경우입니다. 셋째, shadow database를 과신하는 경우입니다. shadow database는 개발 drift 확인에는 좋지만, 운영 데이터 분포, table bloat, lock queue, replica lag를 재현하지 않습니다. 넷째, 백업 확인을 말로만 끝내는 경우입니다.
팀에서는 DB 규칙을 CLAUDE.md에 적어 둡니다. 운영 hot table의 컬럼 삭제는 같은 PR에 넣지 않기, 대규모 백필은 schema migration에 넣지 않기, Prisma migration은 --create-only로 만든 뒤 SQL 리뷰하기, 리뷰에는 공식 문서 링크를 포함하기 같은 규칙입니다. 구성은 CLAUDE.md best practices를 참고하세요.
매출과 연결된 SaaS에서 migration 실패는 기술 부채가 아니라 결제, 가입, 고객 지원 중단으로 이어질 수 있습니다. ClaudeCodeLab의 products에는 재사용 가능한 prompt와 체크리스트가 있고, Claude Code training에서는 실제 저장소에 이 절차를 적용하는 방식을 다룹니다.
이 절차를 실제로 적용했을 때 가장 큰 개선은 SQL이 더 화려해진 것이 아니라, 한 PR이 담당하는 일이 작아진 점이었습니다. expand-only PR, 별도 backfill job, 나중의 contract PR로 나누면 Claude Code의 출력도 사람이 검토하기 쉬워집니다. SQL을 쓰기 전에 중단 조건과 복구 절차를 먼저 쓰게 하는 습관이 운영 사고를 줄입니다.
요약
Claude Code가 프로덕션 DB migration을 자동으로 안전하게 만들지는 않습니다. 하지만 expand/contract, 단계적 앱 배포, 작은 백필, CI 검사, staging 리허설, feature flag, 명확한 롤백 한계를 주면 강력한 리뷰 도구가 됩니다.
다음 migration에서는 먼저 Claude Code에게 위험 리뷰를 요청하세요. 그 계획이 통과한 뒤 SQL, Prisma migration, GitHub Actions 검사, 백필 스크립트를 작성하게 하는 것이 안전한 순서입니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.