본문 바로가기

코딩

django Data Migration Guide

728x90
반응형
Django Migration 완벽 가이드

Django Migration 완벽 가이드

1. 모델 구조 변경 개요

🎯 목표

  • ChatRoom, ChatSession 테이블 제거
  • ChatMessage 테이블을 Parent-Child 구조로 재설계
  • 사용자 질문 (Parent) → AI 답변들 (Child) 1:N 관계
  • 같은 질문에 대한 여러 AI 답변 저장 가능

🔄 변경 전후 비교

🔴 기존 구조
  • ChatRoom
  • ChatSession
  • ChatMessage (단일 구조)
🟢 새로운 구조
  • UserQuestion (Parent)
  • AIResponse (Child)
  • ConversationHistory (선택사항)

2. 기존 테이블 제거 및 새 모델 적용 단계

1 기존 마이그레이션 백업 및 새 마이그레이션 생성
# 1. 현재 마이그레이션 상태 확인 python manage.py showmigrations chat # 2. 새로운 마이그레이션 생성 (기존 테이블 삭제) python manage.py makemigrations chat --empty --name remove_old_tables # 3. 새로운 모델을 위한 마이그레이션 생성 python manage.py makemigrations chat --name create_new_structure
2 기존 테이블 삭제 마이그레이션 파일 수정
# migrations/XXXX_remove_old_tables.py from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('chat', '이전_마이그레이션_번호'), # 실제 번호로 변경 ] operations = [ migrations.RunSQL( "DROP TABLE IF EXISTS chat_sessions;" ), migrations.RunSQL( "DROP TABLE IF EXISTS chat_messages;" ), migrations.RunSQL( "DROP TABLE IF EXISTS chat_rooms;" ), ]
3 새로운 모델 정의
# chat/models.py - 새로운 구조 from django.db import models from django.utils import timezone from django.conf import settings class UserQuestion(models.Model): """사용자 질문 모델 (Parent Table)""" user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="questions", verbose_name="사용자", ) content = models.TextField(verbose_name="질문 내용") question_type = models.CharField( max_length=20, choices=[ ("text", "텍스트"), ("image", "이미지 포함"), ("file", "파일 포함"), ], default="text", verbose_name="질문 유형", ) attached_file = models.FileField( upload_to="question_files/", null=True, blank=True, verbose_name="첨부 파일" ) is_processed = models.BooleanField( default=False, verbose_name="처리됨" ) is_deleted = models.BooleanField( default=False, verbose_name="삭제됨" ) created_at = models.DateTimeField( default=timezone.now, verbose_name="생성일" ) updated_at = models.DateTimeField( auto_now=True, verbose_name="수정일" ) class Meta: db_table = "user_questions" verbose_name = "사용자 질문" verbose_name_plural = "사용자 질문들" ordering = ["-created_at"] def __str__(self): content_preview = ( self.content[:50] + "..." if len(self.content) > 50 else self.content ) return f"{self.user.username}: {content_preview}" class AIResponse(models.Model): """AI 응답 모델 (Child Table)""" question = models.ForeignKey( UserQuestion, on_delete=models.CASCADE, related_name="ai_responses", verbose_name="관련 질문", ) content = models.TextField(verbose_name="AI 응답 내용") ai_model = models.CharField( max_length=50, verbose_name="AI 모델", help_text="예: gpt-4, claude-3, gemini-pro 등" ) response_time = models.FloatField( null=True, blank=True, verbose_name="응답 시간(초)" ) user_rating = models.IntegerField( null=True, blank=True, choices=[(i, f"{i}점") for i in range(1, 6)], verbose_name="사용자 평점 (1-5)" ) is_selected = models.BooleanField( default=False, verbose_name="선택된 응답" ) created_at = models.DateTimeField( default=timezone.now, verbose_name="생성일" ) class Meta: db_table = "ai_responses" verbose_name = "AI 응답" verbose_name_plural = "AI 응답들" ordering = ["question", "-created_at"]

3. 마이그레이션 충돌 해결

⚠️ 주의: 마이그레이션 충돌이 발생하면 데이터 손실 위험이 있습니다. 반드시 백업을 먼저 수행하세요.

🔍 충돌 상황 확인

# 현재 마이그레이션 상태 확인 python manage.py showmigrations chat # 충돌 감지 메시지: # CommandError: Conflicting migrations detected; multiple leaf nodes...

🛠️ 해결 방법 1: 마이그레이션 병합

# 1. 충돌하는 마이그레이션들을 병합 python manage.py makemigrations --merge # 2. 병합된 마이그레이션 적용 python manage.py migrate # 3. 새로운 마이그레이션 생성 python manage.py makemigrations chat --name remove_old_models

🔥 해결 방법 2: 마이그레이션 리셋 (개발 환경)

# 1. 마이그레이션 파일 백업 cp -r chat/migrations chat/migrations_backup # 2. 불필요한 마이그레이션 파일 삭제 rm chat/migrations/0002_*.py rm chat/migrations/0003_*.py # 3. 데이터베이스 마이그레이션 기록 삭제 python manage.py shell

4. 새로운 모델 구조의 장점

✨ 주요 특징

  • 1:N 관계: 하나의 질문에 여러 AI 응답 저장
  • 응답 순서 관리: response_order 필드로 응답 순서 추적
  • 응답 선택: is_selected로 최적 응답 표시
  • 사용자 평가: user_rating으로 응답 품질 평가
  • 성능 추적: response_time으로 성능 모니터링

💻 사용 예시

# 사용자 질문 생성 question = UserQuestion.objects.create( user=request.user, content="Django에서 모델 관계를 어떻게 설정하나요?" ) # 여러 AI 응답 생성 response1 = AIResponse.objects.create( question=question, content="ForeignKey를 사용하여...", ai_model="gpt-4", response_time=2.5 ) response2 = AIResponse.objects.create( question=question, content="다른 방법으로는...", ai_model="claude-3", response_time=1.8 ) # 선택된 응답 표시 response1.is_selected = True response1.save() # 질문의 모든 응답 조회 all_responses = question.ai_responses.all() # 최고 평점 응답 조회 best_response = question.ai_responses.filter( user_rating__isnull=False ).order_by('-user_rating').first()

5. 데이터 백업 (중요!)

🎯 핵심: 마이그레이션 전에 반드시 데이터를 백업하세요!

📦 전체 데이터베이스 백업

# 전체 데이터베이스 백업 python manage.py dumpdata chat > chat_backup.json # 특정 모델별 백업 python manage.py dumpdata chat.ChatMessage > messages_backup.json python manage.py dumpdata chat.ChatRoom > rooms_backup.json python manage.py dumpdata chat.ChatSession > sessions_backup.json # 날짜별 백업 파일명 python manage.py dumpdata chat > chat_backup_$(date +%Y%m%d_%H%M%S).json

🔄 데이터 복원

# 백업 데이터 복원 python manage.py loaddata chat_backup.json # 특정 모델 복원 python manage.py loaddata messages_backup.json

6. 문제 해결 팁

🚨 일반적인 문제들

1 마이그레이션이 계속 실패하는 경우
# 마이그레이션 상태를 가짜로 표시 (주의해서 사용) python manage.py migrate chat --fake # 또는 특정 마이그레이션으로 되돌리기 python manage.py migrate chat 0001 --fake # 마이그레이션 히스토리 확인 python manage.py showmigrations --verbosity=2
2 외래키 제약조건 오류
# 외래키 제약조건을 무시하고 삭제 from django.db import migrations class Migration(migrations.Migration): operations = [ # MySQL용 migrations.RunSQL("SET foreign_key_checks = 0;"), migrations.RunSQL( "DROP TABLE IF EXISTS chat_sessions;" ), migrations.RunSQL( "DROP TABLE IF EXISTS chat_messages;" ), migrations.RunSQL( "DROP TABLE IF EXISTS chat_rooms;" ), migrations.RunSQL("SET foreign_key_checks = 1;"), ]
3 SQLite에서 테이블 삭제 시
# SQLite에서는 다음 명령어 사용 python manage.py dbshell # SQLite shell에서: .tables DROP TABLE IF EXISTS chat_sessions; DROP TABLE IF EXISTS chat_messages; DROP TABLE IF EXISTS chat_rooms; .quit
4 PostgreSQL에서 CASCADE 삭제
# PostgreSQL에서 관련 테이블까지 함께 삭제 DROP TABLE IF EXISTS chat_sessions CASCADE; DROP TABLE IF EXISTS chat_messages CASCADE; DROP TABLE IF EXISTS chat_rooms CASCADE;

⚠️ 주의사항

  • 운영 환경에서는 반드시 백업 후 마이그레이션을 수행하세요
  • 대용량 데이터가 있는 경우 마이그레이션 시간이 오래 걸릴 수 있습니다
  • 외래키 제약조건으로 인해 테이블 삭제 순서가 중요합니다
  • 마이그레이션 전후로 데이터 무결성을 검증하세요

7. 실전 마이그레이션 예제

🎯 완전한 마이그레이션 프로세스

# 1. 현재 상태 확인 python manage.py showmigrations # 2. 충돌 해결 (필요시) python manage.py makemigrations --merge python manage.py migrate # 3. 데이터 백업 python manage.py dumpdata chat > backup_before_migration.json # 4. 기존 모델 제거 마이그레이션 python manage.py makemigrations chat --empty --name remove_old_models # 5. 새 모델 생성 마이그레이션 python manage.py makemigrations chat --name create_new_structure # 6. 마이그레이션 적용 python manage.py migrate # 7. 새 구조 확인 python manage.py dbshell .tables # SQLite의 경우 \dt # PostgreSQL의 경우

🔍 마이그레이션 검증

# 마이그레이션 후 데이터 검증 from chat.models import UserQuestion, AIResponse # 기본 통계 확인 print(f"총 질문 수: {UserQuestion.objects.count()}") print(f"총 응답 수: {AIResponse.objects.count()}") print(f"처리된 질문 수: {UserQuestion.objects.filter(is_processed=True).count()}") # 응답별 통계 response_stats = {} for response in AIResponse.objects.values('ai_model').distinct(): model = response['ai_model'] count = AIResponse.objects.filter(ai_model=model).count() response_stats[model] = count print("AI 모델별 응답 통계:", response_stats) # 질문당 평균 응답 수 if UserQuestion.objects.count() > 0: avg_responses = AIResponse.objects.count() / UserQuestion.objects.count() print(f"질문당 평균 응답 수: {avg_responses:.2f}") # 선택된 응답 통계 selected_count = AIResponse.objects.filter(is_selected=True).count() print(f"선택된 응답 수: {selected_count}")

🔧 단계별 마이그레이션 파일 예제

1 기존 모델 제거 마이그레이션
# migrations/0003_remove_old_models.py from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('chat', '0002_alter_chatmessage_ai_model'), ] operations = [ # 외래키 제약조건 때문에 순서가 중요합니다 migrations.DeleteModel(name='ChatSession'), migrations.DeleteModel(name='ChatMessage'), migrations.DeleteModel(name='ChatRoom'), ]
2 새 모델 생성 마이그레이션
# migrations/0004_create_new_structure.py from django.conf import settings from django.db import migrations, models import django.db.models.deletion import django.utils.timezone class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('chat', '0003_remove_old_models'), ] operations = [ migrations.CreateModel( name='UserQuestion', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('content', models.TextField(verbose_name='질문 내용')), ('question_type', models.CharField(choices=[('text', '텍스트'), ('image', '이미지 포함'), ('file', '파일 포함')], default='text', max_length=20, verbose_name='질문 유형')), ('attached_file', models.FileField(blank=True, null=True, upload_to='question_files/', verbose_name='첨부 파일')), ('is_processed', models.BooleanField(default=False, verbose_name='처리됨')), ('is_deleted', models.BooleanField(default=False, verbose_name='삭제됨')), ('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='생성일')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='수정일')), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questions', to=settings.AUTH_USER_MODEL, verbose_name='사용자')), ], options={ 'verbose_name': '사용자 질문', 'verbose_name_plural': '사용자 질문들', 'db_table': 'user_questions', 'ordering': ['-created_at'], }, ), migrations.CreateModel( name='AIResponse', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('content', models.TextField(verbose_name='AI 응답 내용')), ('ai_model', models.CharField(help_text='예: gpt-4, claude-3, gemini-pro 등', max_length=50, verbose_name='AI 모델')), ('response_time', models.FloatField(blank=True, null=True, verbose_name='응답 시간(초)')), ('user_rating', models.IntegerField(blank=True, choices=[(1, '1점'), (2, '2점'), (3, '3점'), (4, '4점'), (5, '5점')], null=True, verbose_name='사용자 평점 (1-5)')), ('is_selected', models.BooleanField(default=False, verbose_name='선택된 응답')), ('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='생성일')), ('question', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ai_responses', to='chat.userquestion', verbose_name='관련 질문')), ], options={ 'verbose_name': 'AI 응답', 'verbose_name_plural': 'AI 응답들', 'db_table': 'ai_responses', 'ordering': ['question', '-created_at'], }, ), ]

🎉 마이그레이션 완료 후 확인

# Django Admin에서 새 모델 확인 python manage.py createsuperuser # 필요시 python manage.py runserver # 또는 Django shell에서 테스트 python manage.py shell
# Django shell에서 테스트 from django.contrib.auth.models import User from chat.models import UserQuestion, AIResponse # 테스트 사용자 생성 또는 가져오기 user = User.objects.first() if not user: user = User.objects.create_user( 'testuser', 'test@example.com', 'password' ) # 테스트 질문 생성 question = UserQuestion.objects.create( user=user, content="Django 마이그레이션이 잘 작동하나요?", question_type="text" ) # 테스트 응답 생성 response = AIResponse.objects.create( question=question, content="네, 완벽하게 작동합니다!", ai_model="test-model", response_time=1.5 ) print(f"질문: {question}") print(f"응답: {response}") # 관계 확인 print("질문의 모든 응답:", question.ai_responses.all()) print("응답의 질문:", response.question)

✅ 체크리스트

  • ✅ 데이터 백업 완료
  • ✅ 마이그레이션 충돌 해결
  • ✅ 기존 테이블 제거
  • ✅ 새 모델 구조 적용
  • ✅ 데이터 무결성 검증
  • ✅ 기능 테스트 완료
728x90
반응형