마이그레이션 전략
Django 마이그레이션은 모델 변경 사항을 데이터베이스에 반영하는 버전 관리 시스템입니다. 프로덕션 환경에서는 신중하게 적용해야 합니다.
기본 마이그레이션 흐름
# 1. 모델 변경 후 마이그레이션 파일 생성
python manage.py makemigrations
# 2. 마이그레이션 내용 확인 (SQL 미리보기)
python manage.py sqlmigrate products 0001
# 3. DB에 적용
python manage.py migrate
# 4. 상태 확인
python manage.py showmigrations
python manage.py showmigrations products
안전한 스키마 변경
# 1단계: 새 컬럼 추가 — null=True로 시작
# products/models.py
class Product(models.Model):
# 기존 필드들...
discount_rate = models.DecimalField(
max_digits=5, decimal_places=2,
null=True, blank=True, # ✅ null 허용으로 시작
default=0.0,
)
python manage.py makemigrations --name "add_discount_rate_to_product"
python manage.py migrate
# 2단계: 기존 데이터 채우기 (데이터 마이그레이션)
# python manage.py makemigrations --empty products --name "fill_discount_rate"
# products/migrations/0003_fill_discount_rate.py
from django.db import migrations
def fill_discount(apps, schema_editor):
Product = apps.get_model("products", "Product")
Product.objects.filter(discount_rate__isnull=True).update(discount_rate=0.0)
def unfill_discount(apps, schema_editor):
"""rollback용 reverse function"""
pass
class Migration(migrations.Migration):
dependencies = [("products", "0002_add_discount_rate_to_product")]
operations = [
migrations.RunPython(fill_discount, unfill_discount),
]
# 3단계: null 제거 (별도 마이그레이션)
class Product(models.Model):
discount_rate = models.DecimalField(
max_digits=5, decimal_places=2,
default=0.0, # null=True, blank=True 제거
)
복잡한 스키마 변경 패턴
# ① 컬럼 이름 변경 — RenameField
# migrations/0004_rename_price.py
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("products", "0003_fill_discount_rate")]
operations = [
migrations.RenameField(
model_name="product",
old_name="price",
new_name="selling_price",
),
]
# ② 테이블 분리 (데이터 이동 포함)
def split_address(apps, schema_editor):
User = apps.get_model("users", "User")
Address = apps.get_model("users", "Address")
for user in User.objects.all():
if user.address_text: # 기존 필드
Address.objects.create(
user=user,
street=user.address_text,
city="",
)
class Migration(migrations.Migration):
operations = [
# 1. 새 테이블 생성
migrations.CreateModel(
name="Address",
fields=[
("id", ...),
("user", ...),
("street", ...),
],
),
# 2. 데이터 이전
migrations.RunPython(split_address),
# 3. 기존 필드 제거 (충분한 검증 후)
# migrations.RemoveField(model_name="user", name="address_text"),
]
마이그레이션 충돌 해결
# 두 개발자가 동시에 makemigrations 했을 때
# 0003_alice.py와 0003_bob.py 두 파일 생성 → 충돌
# 자동 병합 시도
python manage.py makemigrations --merge
# 생성된 merge 파일 확인 후 적용
python manage.py migrate
products/
├── 0001_initial.py
├── 0002_add_discount_rate.py
├── 0003_alice.py ← 충돌
├── 0003_bob.py ← 충돌
└── 0004_merge.py ← 병합 파일
대형 테이블 마이그레이션
# 수천만 건 테이블: 인덱스 생성은 테이블 락 발생
# django-pg-zero-downtime-migrations 사용 또는 수동 처리
# ✅ database_backends: 비동시 인덱스 생성 (PostgreSQL)
from django.db import migrations, models
class Migration(migrations.Migration):
atomic = False # ← 트랜잭션 비활성화 (인덱스 생성용)
operations = [
migrations.SeparateDatabaseAndState(
database_operations=[
migrations.RunSQL(
"CREATE INDEX CONCURRENTLY idx_product_price ON products_product(price);",
"DROP INDEX IF EXISTS idx_product_price;",
)
],
state_operations=[
migrations.AddIndex(
model_name="product",
index=models.Index(fields=["price"], name="idx_product_price"),
)
],
)
]
마이그레이션 모범 사례
✅ 마이그레이션 파일은 반드시 git에 커밋
✅ makemigrations 후 sqlmigrate로 SQL 확인
✅ 프로덕션 배포 전 스테이징에서 테스트
✅ 대형 테이블은 오프피크 시간에 적용
✅ 컬럼 삭제는 여러 단계로 (코드 제거 → null 허용 → 실제 삭제)
❌ squashmigrations 남용 (히스토리 손실)
❌ 마이그레이션 파일 수동 편집 후 migrate 없이 배포
❌ 배포와 동시에 대형 마이그레이션 적용
정리
| 명령 | 설명 |
|---|---|
makemigrations | 마이그레이션 파일 생성 |
migrate | DB에 적용 |
sqlmigrate | SQL 미리보기 |
showmigrations | 적용 상태 확인 |
makemigrations --merge | 충돌 병합 |
RunPython | 데이터 마이그레이션 |
SeparateDatabaseAndState | DB/상태 분리 처리 |
안전한 마이그레이션은 작게, 단계적으로, 롤백 계획과 함께 진행하는 것이 핵심입니다.