Intro
Django의 Migrations는 SQL 혹은 데이터베이스에 대한 깊은 지식 없이도 간단한 명령어를 통해 데이터베이스에 테이블을 생성하고 수정하고 삭제하는 일련의 작업을 손쉽게 할 수 있도록 도와줍니다.
더 나아가 Migrations을 이용하면 테이블 생성과 같은 SQL(DDL)을 수행하면서 python 또는 SQL을 통해 초기 값을 업데이트하는 등의 작업을 할 수 있습니다.
그런데 프로젝트를 진행하다보면 종종 Migrations 때문에 고통받는 일이 발생합니다.
평소처럼 makemigrations와 migrate 명령어를 실행했는데 원인 모를 에러가 발생했을 때 Migrations에 대한 정확한 이해가 없다면 현 상황에 대한 정확한 진단이 어려워 문제를 해결하기 매우 어렵습니다.
특히 데이터베이스로 MySQL을 사용하고 있다면 MySQL은 transaction support를 지원하지 않기 때문에 reverse migrate 등을 수행할 때 문제를 일으킬 가능성이 더욱 높습니다.
migration이 꼬였을 때 초보자들은 흔히 데이터베이스를 전부 날리고 모든 테이블을 새로 생성하곤 합니다.
테이블을 삭제하면 그 동안 쌓인 데이터도 함께 삭제되기 때문에 운영 중인 서비스에서 이런 방식으로 문제를 해결할 순 없습니다.
fake initial를 통해서 스키마만 다시 만들어서 적용하는 방법도 있습니다.
이번 포스트에서는 Migrations에 대해 자세히 알아보고 Migrations를 사용하며 발생할 수 있는 문제들에 대해 다뤄보겠습니다.
Migrations
Migrations를 비유하여 설명하기 가장 좋은 예시는 Git과 같은 Version-control system입니다.
완벽히 대응되지는 않습니다!
Git이 snapshot을 통해 source code의 변화를 관리하는 것처럼 Migrations 시스템은 앱마다 존재하는 migrations 패키지 안의 파일을 통해 모델의 변화를 탐지하고 데이터베이스를 관리합니다.
(예시: app/migrations/0001_initial.py)
1) 새로운 모델의 경우, 데이터베이스에 테이블을 생성하고 각 컬럼을 정의하는 migration 스크립트를
2) 기존에 존재하던 모델의 경우, dependency를 확인하여 테이블을 수정하는 migration 스크립트를
3) 모델을 삭제하는 경우, 테이블을 삭제하는 migration 스크립트를 생성합니다.
migrations 패키지 안에 쌓인 migration 파일들은 python manage.py migrate 명령어를 통해 migration 파일의 내용에 맞는 SQL로 mapping 되어 데이터베이스에 적용됩니다.
python manage.py sqlmigrate <app_name> <migration>를 입력해보면 실제 적용되는 SQL을 확인할 수 있습니다.
Dependency
먼저 migration 파일의 의존성(dependency)에 대해 알아봅시다.
테이블 사이에는 관계(relation)가 존재합니다.
예를 들어 유저를 관리하는 User 테이블이 있고 주문 내역을 관리하는 Order 테이블이 있다고 합시다.
Order를 생성하기 위해서는 반드시 User가 필요하고 User와 Order는 ForeignKey 관계로 연결되어 있습니다.
위 내용을 모델로 표현하면 다음과 같습니다:
from django.db import models
class User(models.Model):
pass
class Order(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
Python
복사
이런 경우 테이블 생성 순서가 매우 중요합니다.
Order 테이블은 User를 참조하고 있으므로 Order 테이블이 생성되기 위해서는 반드시 User 테이블이 먼저 생성 되어 있어야 합니다.
여러 모델 간의 관계가 복잡한 경우 모든 migration을 한 번에 읽고 테이블을 생성하는 것이 어렵습니다.
따라서 migration에는 이런 문제를 해결하기 위해 dependency가 존재합니다.
Order를 정의하는 migration 파일은 User를 정의하는 migration 파일을 dependency로 참고하고 있습니다.
dependency로 인해 Order 테이블을 생성하는 SQL은 User 테이블이 생성된 이후에 실행됩니다.
dependency는 migration 파일 안에 아래와 같이 명시되어 있습니다:
class Migration(migrations.Migration):
dependencies = [
('foo', '0001_initial'),
]
SQL
복사
위의 migration은 foo 앱의 0001_initial migration을 dependency로 삼고 있습니다.
django_migrations 테이블
migrate 명령어를 통해 데이터베이스에 migration이 적용될 때 Django는 django_migrations 테이블에 migration이 적용된 내역을 기록합니다.
착각해서 안 되는 점은 migration의 내역을 기록한다는 것이 데이터베이스에 적용된 migration의 순서, 이름, 시간 등의 정보를 기록한다는 것이지 migration 파일의 내용이 데이터베이스에 저장되는 것은 아닙니다.
즉 데이터베이스에는 어떤 migration이 언제 어떤 순서로 적용되었다는 사실만 기록하고, 해당 migration의 내용은 데이터베이스에서 관리하지 않습니다.
따라서 이미 적용된 migration에 대해서 migration 파일의 내용을 수정하면 수정한 내용이 데이터베이스에 반영되지 않습니다.
위의 경우에 migrate 명령어를 수행해도 이미 django_migrations 테이블에 해당 migration이 적용된 내역이 존재하기 때문에 migration이 동작하지 않습니다.
이미 적용된 migration 파일을 수정하면 migration 파일이 데이터베이스의 상태를 올바르게 나타내지 못합니다.
그러므로 이미 적용된 migration을 수정하는 일은 없어야 합니다.
migration 문제가 발생했을 때는 migration 파일의 내용과 더불어 django_migrations 테이블을 확인해야 현재 데이터베이스에 적용된 migration의 상태(순서, 적용 여부)를 정확히 파악할 수 있습니다.
python manage.py showmigrations 명령어를 통해서도 현재 migration의 상태를 확인할 수 있지만 migration이 꼬여 에러가 발생하는 경우에는 showmigrations가 현재 데이터베이스의 상태를 온전히 표현하지 않을 수 있기 때문에 직접 테이블을 조회하고 삭제하는 방법을 반드시 알고 있어야 합니다.
Revert
만약 데이터베이스에 적용된 내용을 되돌리고 싶으면 어떻게 해야 할까요?
migrate 명령어를 이용하면 적용된 migration도 revert 할 수 있습니다.
다만 이 때는 되돌리고자 하는 앱과 migration 파일의 번호를 명시해야 합니다. git reset을 생각하면 됩니다.
예를 들어 현재 tutorial 앱에 0010_auto_20210204_0001.py까지 적용되어 있다고 하겠습니다.
이 때 python manage.py migrate tutorial 0009를 실행하면 0010번 migration은 unapply 되고 0009번 기준으로 데이터베이스가 돌아갑니다.
migration을 되돌릴 때 유의해야 하는 점은 0009로 돌리려고 할 때 revert가 완료되기 전까지는 0010 migration 파일을 지워서는 안됩니다.
migration을 0009로 revert 할 때 0010을 지우고 명령어를 실행하면 revert가 수행되지 않습니다.
실수로 unapply 해야하는 migration을 먼저 지웠을 경우, 다시 동일한 내용의 migration을 만들어주면 됩니다.
만약 지운 migration을 복원할 수 없다면 직접 데이터베이스를 조작해야 합니다.
RunPython을 이용하면 migration을 적용하면서 django 코드를 실행할 수 있습니다.
예를 들어 모델에 새로운 필드를 추가한 뒤 곧바로 모든 cache를 flush 해야하는 상황이 있다고 해봅시다.
이런 경우 새롭게 적용될 migration에 다음과 같은 코드를 추가하면 됩니다.
from django.core.cache import cache
from django.db import migrations, models
def flush_cache(apps, schema_editor):
cache().clear()
class Migration(migrations.Migration):
dependencies = [
('user', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='new_field',
field=models.CharField(max_length=8),
),
migrations.RunPython(flush_cache, migrations.RunPython.noop),
]
Python
복사
위의 예시처럼 python으로 수행해야하는 작업이 있는 경우 새로운 함수를 정의한 뒤 RunPython에 첫 번째 argument로 전달합니다.
RunPython의 두 번째 argument로는 revert 과정에 동작시킬 함수를 전달하면 됩니다.
revert 할 때 별다른 작업이 필요하지 않은 경우에는 예시처럼 migrations.RunPython.noop을 전달하면 됩니다.
Case study
몇 가지 상황을 가정하고 문제를 해결하는 방법에 대해 간단하게 설명하겠습니다.
migrate 된 내용을 수정하려고 할 때
user 앱의 0002 migration이 적용된 상태에서 적용된 내용을 수정하고 싶을 때 가장 쉬운 방법은 모델을 수정하고 makemigration을 통해 새로운 0003 migration을 생성하여 적용하는 것입니다.
그런데 새로운 migration을 생성하지 않고 0002 migration을 수정하고 싶다면 어떻게 할까요?
먼저 데이터베이스를 이전 상태로 되돌려야합니다.
Revert 파트에서 설명했지만 unapply 하려는 migration을 먼저 삭제하면 안됩니다.
0002 migration 파일은 그대로 둔채 다음의 명령어를 먼저 실행합니다:
>>> python manage.py migrate user 0001
Python
복사
migration이 0001로 정상적으로 revert 됐다면 0002 migration을 삭제하고 모델을 수정한 뒤 makemigration으로 0002 migration을 새로 생성한 뒤 migrate 해주면 됩니다.
MySQL transaction rollback issue
데이터베이스로 MySQL을 사용하고 있다면 빈번하게 발생하는 문제입니다.
예를 들어 테이블에 새로운 필드를 추가하면서 RunPython을 함께 실행한다고 하겠습니다.
그런데 RunPython 코드에 문제가 있어서 에러가 발생했다고 합시다.
이 상황에서 django_migrations 테이블을 조회해보면 문제가 된 migration은 적용되지 않은 걸로 보입니다.
그런데 migration 파일의 에러를 수정한 뒤 다시 migrate 해보면 django.db.utils.OperationalError: (1060, "Duplicate column name 'xxx'") 에러가 발생합니다.
이전에 적용했던 migration이 실패했지만 새로운 컬럼이 생성된 다음 rollback 되지 않은 것입니다.
문제가 된 테이블을 직접 열어보면 필드가 생성되어 있습니다.
django_migrations 테이블의 내용이 실제 데이터베이스의 상태를 제대로 표현하고 있지 못하는 상황입니다.
이런 경우 생성된 열을 직접 삭제해서 다시 데이터베이스를 상태를 동기화하여야 합니다.
ALTER TABLE table_name DROP COLUMN column_name;
SQL
복사
필드를 추가할 때 뿐 아니라 테이블을 생성한 뒤 roll back 할 때도 동일한 현상이 발생합니다.
마찬가지로 생성된 table을 직접 DROP 하면 해결할 수 있습니다.