본문으로 건너뛰기
Advertisement

마이그레이션 전략

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마이그레이션 파일 생성
migrateDB에 적용
sqlmigrateSQL 미리보기
showmigrations적용 상태 확인
makemigrations --merge충돌 병합
RunPython데이터 마이그레이션
SeparateDatabaseAndStateDB/상태 분리 처리

안전한 마이그레이션은 작게, 단계적으로, 롤백 계획과 함께 진행하는 것이 핵심입니다.

Advertisement