docs(10_Wiki): 위키 전체 재구성 — Topic_* 폴더를 4개 카테고리로 통합 + 대규모 중복 제거
Topic_Agent/Topic_Blog/Topics/Topics_Biz/Topics_Meeting/Topics_Rag의 마크다운 지식 문서를 Topic_General/Topic_Programming/Topic_Graphic/Topic_Business 4개 카테고리로 재분류. - 중복 제거: frontmatter의 status:duplicate/merged + duplicate_of/redirect_to 필드로 자기 자신을 중복으로 선언한 리다이렉트 stub 1032개 제거, 완전 동일 내용 파일 472개 제거, 동일 파일명·다른 내용 충돌 시 더 큰(완전한) 버전만 유지(162개 제거) — 총 1639개 중복 제거. - 분류: 폴더 단위로 명확한 항목(AI_and_ML/Coding/Architecture 등 → Programming, Comfyui/Visual_Effects → Graphic, Topics_Biz/Topics_Meeting/사업 등 → Business, Poetic_Blog_Writing/창의성/Game_Design 등 → General)은 폴더 우선순위로, 나머지 혼재 폴더(Topic_Agent/Topic_Blog/Topics 루트/Thinking & Reasoning/Other/UI_UX_Assets)는 title/tags 키워드 스코어링으로 파일 단위 분류(불명확한 경우 General로 폴백). 원본 폴더명은 "From_*" 서브폴더로 보존해 추적 가능성 유지. - 최종 배치: Programming 2784 / General 1608 / Graphic 285 / Business 249 = 4926개 문서. - 에이전트 운영 상태(.astra/.agent/.obsidian/sessions/memory/_company/docs/lessons/_shared/src)는 지식 콘텐츠가 아니므로 재분류 대상에서 제외하고 원위치 유지. - Topics/Topic_email(상위 보호 폴더 Topic_email과 파일명 100% 중복) 삭제 — 보호 폴더 자체는 미변경. - 완전히 비게 된 Topic_Agent/Topic_Blog/Topics_Biz/Topics_Rag 폴더 제거.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
id: db-migration-safety
|
||||
title: DB Migration Safety — Zero-downtime 전환 패턴
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [database, migration, zero-downtime, postgres, vibe-coding]
|
||||
tech_stack: { language: "SQL / Postgres / MySQL", applicable_to: ["Backend"] }
|
||||
applied_in: []
|
||||
aliases: [expand-contract, blue-green schema, online migration, lock-free]
|
||||
---
|
||||
|
||||
# DB Migration Safety
|
||||
|
||||
> **Expand → Migrate → Contract** 3단계가 정답. 한 번에 schema + 코드 둘 다 못 바꾼다 — 둘이 동시에 deploy 안 되니까. NOT NULL 추가 / column rename / type change 가 가장 위험.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- **Expand**: 새 schema 추가. 옛 코드는 옛 schema 만 사용. 양립 가능.
|
||||
- **Migrate**: 코드를 새 schema 로 이전. dual-write 또는 backfill.
|
||||
- **Contract**: 옛 schema 제거.
|
||||
- 각 단계는 별도 deploy. 사이에 모니터링 시간.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Column rename `old_name → new_name`
|
||||
|
||||
#### Expand
|
||||
```sql
|
||||
ALTER TABLE users ADD COLUMN new_name TEXT;
|
||||
-- Trigger: old_name 변경 시 new_name 동기화
|
||||
CREATE OR REPLACE FUNCTION sync_name() RETURNS trigger AS $$
|
||||
BEGIN NEW.new_name := COALESCE(NEW.new_name, NEW.old_name); RETURN NEW; END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
CREATE TRIGGER trg_sync_name BEFORE INSERT OR UPDATE ON users
|
||||
FOR EACH ROW EXECUTE PROCEDURE sync_name();
|
||||
|
||||
-- Backfill (낮 시간대 batch)
|
||||
UPDATE users SET new_name = old_name WHERE new_name IS NULL;
|
||||
```
|
||||
|
||||
#### Migrate (코드 deploy)
|
||||
- 코드: 읽기는 `COALESCE(new_name, old_name)`, 쓰기는 둘 다.
|
||||
|
||||
#### Contract
|
||||
```sql
|
||||
DROP TRIGGER trg_sync_name ON users;
|
||||
DROP FUNCTION sync_name();
|
||||
ALTER TABLE users DROP COLUMN old_name;
|
||||
```
|
||||
|
||||
### NOT NULL 추가 — 큰 테이블
|
||||
|
||||
```sql
|
||||
-- ❌ 직접 NOT NULL — 대형 테이블 long lock
|
||||
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;
|
||||
|
||||
-- ✅ 단계별
|
||||
-- 1) DEFAULT + NOT VALID CHECK
|
||||
ALTER TABLE orders ADD CONSTRAINT orders_status_chk CHECK (status IS NOT NULL) NOT VALID;
|
||||
-- 2) 기존 행 backfill
|
||||
UPDATE orders SET status = 'pending' WHERE status IS NULL;
|
||||
-- 3) constraint validate (read lock 만)
|
||||
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_chk;
|
||||
-- 4) 진짜 NOT NULL (Postgres 12+ — fast path)
|
||||
ALTER TABLE orders ALTER COLUMN status SET NOT NULL;
|
||||
ALTER TABLE orders DROP CONSTRAINT orders_status_chk;
|
||||
```
|
||||
|
||||
### Index 추가 — CONCURRENTLY (Postgres)
|
||||
```sql
|
||||
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
|
||||
-- write block 안 함. 단 transaction 안에서는 못 씀.
|
||||
```
|
||||
|
||||
### 큰 테이블 backfill — batch
|
||||
```sql
|
||||
-- 한 번에 1000행씩
|
||||
DO $$
|
||||
DECLARE batch INT := 1000;
|
||||
BEGIN
|
||||
LOOP
|
||||
WITH ids AS (
|
||||
SELECT id FROM orders WHERE new_field IS NULL LIMIT batch FOR UPDATE SKIP LOCKED
|
||||
)
|
||||
UPDATE orders SET new_field = compute(id) WHERE id IN (SELECT id FROM ids);
|
||||
EXIT WHEN NOT FOUND;
|
||||
PERFORM pg_sleep(0.1);
|
||||
END LOOP;
|
||||
END$$;
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 변경 | 안전 |
|
||||
|---|---|
|
||||
| Column 추가 (nullable) | ✅ 즉시 |
|
||||
| Column 추가 + DEFAULT (Postgres 11+) | ✅ — fast path |
|
||||
| NOT NULL 추가 | expand-contract |
|
||||
| Column rename | expand-contract |
|
||||
| Type change (varchar → text) | 보통 안전 — but 큰 테이블이면 점검 |
|
||||
| Type change (int → bigint) | expand-contract |
|
||||
| Drop column | expand-contract — 코드 먼저 안 쓰게 |
|
||||
| Index 추가 | CONCURRENTLY |
|
||||
| FK 추가 | NOT VALID + VALIDATE |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **schema 변경과 코드 변경 한 deploy**: 둘이 정확히 동시 시작 못 함. 한 쪽만 적용된 짧은 시간 = 사고.
|
||||
- **online migration 없는 큰 테이블 ALTER**: 수십 분 lock → 다운타임.
|
||||
- **rollback 계획 없음**: 새 코드 + 새 schema deploy 후 문제 → 옛 코드 + 새 schema 인스턴스 발생 → 사고. expand-contract 면 자연 롤백.
|
||||
- **production 에 직접 SQL**: history / review 없음. migration tool (Flyway / Prisma migrate / golang-migrate / dbmate) 사용.
|
||||
- **migration 안에 비즈니스 로직**: 한 트랜잭션에 무거운 변환. 별도 backfill batch.
|
||||
- **trigger 가 영구 남음**: contract 단계에서 trigger 제거 잊음.
|
||||
- **dev 에서만 테스트**: 데이터 양이 다름. staging with prod-size dataset 검증.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- "schema 변경 = expand-migrate-contract 3 deploy" 강제.
|
||||
- 큰 테이블 ALTER 는 항상 단계별 + CONCURRENTLY 권장.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[DB_Connection_Pool]]
|
||||
- [[Optimistic_Concurrency_Control]]
|
||||
Reference in New Issue
Block a user