사이드 프로젝트를 하다가 serializer의 create 메서드에서 두 인스턴스를 동시에 저장해야했다. 좀 더 구체적으로 얘기하면 User와 Profile을 동시에 저장했어야했다. 하지만 중간에 오류가 발생하면 User만 저장되고 Profile은 저장이 안 될 수도 있다. 동시 저장이 보장되는 방법으로 트랜잭션이 떠올라서 공식 문서에서 내용을 찾아보고 직접 실험도 해봤다. 그래서 해당 내용을 정리해보려고 한다. 단, on_commit
으로 직접 저장 지점을 지정하는 부분은 다루지 않기 때문에 해당 내용에 관심이 있으면 공식 문서나 다른 포스팅을 참고하길 바란다 😓
먼저 공식 문서에 나와있는 내용을 살펴보자. 공식 문서를 보고싶다면 여기를 클릭해라. 아래에 설명하는 부분도 모두 공식 문서를 참고한 것을 미리 밝힌다.
장고의 기본 트랜잭션 행동
장고는 기본적으로 오토 커밋 모드이다. 그래서 트랜잭션이 active 하지 않다면 즉시 커밋된다. 좀 더 쉽게 얘기하면 create 이런 거를 호출하면 따로 커밋을 하지 않아도 바로 반영이 된다는 것이다.
그리고 트랜잭션에 각각의 리퀘스트를 감싸는 것은 기본적으로 비활성화돼있다. 해당 설정을ATOMIC_REQUESTS
라고 한다. 만약 이게 활성화돼있다면 장고는 다음과 같이 작동한다. view 함수를 부르기 전에 트랜잭션을 시작하고 응답을 생성할 때 문제가 없다면 트랜잭션을 커밋하고 그렇지 않다면 트랜잭션을 롤백한다. (성능 문제가 있을 수 있는데 이건 이번 포스팅에서 논외로 한다. 궁금하면 공식 문서의 여기에서 경고를 봐보자)
트랜잭션 직접 지정하기
물론 설정에서 ATOMIC_REQUESTS
를 True
로 활성화할 수도 있지만 이전에 언급한 것처럼 성능 문제가 있을 수 있다. 그래서 메서드 내에서 직접 스코프로 지정하거나 해당 메서드에 데코레이터로 지정할 수 있다.
atomic
매니저 이용하기
with transaction.atomic():
// execute queries
- 데코레이터 이용하기
@transaction.atomic
def method():
실제 코드로 테스트
직접 코드로 각각의 경우를 테스트 해보자. 위에서 언급했던 것처럼 serializer에서 User와 Profile을 동시에 저장해야하는데 중간에 에러가 난 시나리오라고 해보자. 각각의 케이스는 DRF browsable API에서 유저를 생성하는 앤드포인트에서 다음과 같이 유저 이름 및 필요한 정보를 넣고 POST 요청을 했다.
그리고 shell에서 User와 Profile을 확인했다.
아무런 설정도 안 한 경우
class RegisterSerializer(serializers.ModelSerializer):...def create(self, validated_data):
user = User.objects.create_user(
username=validated_data["username"],
email=validated_data["email"],
first_name=validated_data["first_name"],
last_name=validated_data["last_name"]
) user.set_password(validated_data["password"])
user.save()
raise AssertionError() // 에러 발생! Profile.objects.create(user=user) return user
이런 경우 User만 저장되고 프로필은 저장되지 않는다 😨
In [2]: user = User.objects.get(username="test9")In [3]: user.profile
---------------------------------------------------------------------------
RelatedObjectDoesNotExist Traceback (most recent call last)
<ipython-input-3-ca179ca5cd11> in <module>
----> 1 user.profile~/github/medium-clone/medium_clone/env/lib/python3.9/site-packages/django/db/models/fields/related_descriptors.py in __get__(self, instance, cls)
419
420 if rel_obj is None:
--> 421 raise self.RelatedObjectDoesNotExist(
422 "%s has no %s." % (
423 instance.__class__.__name__,RelatedObjectDoesNotExist: User has no profile.
ATOMIC_REQUESTS
를 True로 한 경우
- settings.py
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': BASE_DIR / 'db.sqlite3', 'ATOMIC_REQUESTS': True }}
create
는 아무런 설정도 안 한 경우와 같아서 생략한다.
유저가 저장되지 않은 것을 알 수 있다.
In [2]: user = User.objects.get(username="test9")
---------------------------------------------------------------------------
DoesNotExist Traceback (most recent call last)
<ipython-input-2-f1258946737b> in <module>
----> 1 user = User.objects.get(username="test9")~/github/medium-clone/medium_clone/env/lib/python3.9/site-packages/django/db/models/manager.py in manager_method(self, *args, **kwargs)
83 def create_method(name, method):
84 def manager_method(self, *args, **kwargs):
---> 85 return getattr(self.get_queryset(), name)(*args, **kwargs)
86 manager_method.__name__ = method.__name__
87 manager_method.__doc__ = method.__doc__~/github/medium-clone/medium_clone/env/lib/python3.9/site-packages/django/db/models/query.py in get(self, *args, **kwargs)
427 return clone._result_cache[0]
428 if not num:
--> 429 raise self.model.DoesNotExist(
430 "%s matching query does not exist." %
431 self.model._meta.object_nameDoesNotExist: User matching query does not exist.
transaction manager 혹은 데코레이터를 이용한 경우
class RegisterSerializer(serializers.ModelSerializer):...def create(self, validated_data): with transaction.atomic():
user = User.objects.create_user(
username=validated_data["username"],
email=validated_data["email"],
first_name=validated_data["first_name"],
last_name=validated_data["last_name"]
) user.set_password(validated_data["password"])
user.save()
raise AssertionError() // 에러 발생! Profile.objects.create(user=user) return userclass RegisterSerializer(serializers.ModelSerializer):...@trasaction.atomic
def create(self, validated_data):
user = User.objects.create_user(
username=validated_data["username"],
email=validated_data["email"],
first_name=validated_data["first_name"],
last_name=validated_data["last_name"]
) user.set_password(validated_data["password"])
user.save()
raise AssertionError() // 에러 발생! Profile.objects.create(user=user) return user
마찬가지로 유저가 저장되지 않았다.
In [2]: user = User.objects.get(username="test9")
---------------------------------------------------------------------------
DoesNotExist Traceback (most recent call last)
<ipython-input-2-f1258946737b> in <module>
----> 1 user = User.objects.get(username="test9")~/github/medium-clone/medium_clone/env/lib/python3.9/site-packages/django/db/models/manager.py in manager_method(self, *args, **kwargs)
83 def create_method(name, method):
84 def manager_method(self, *args, **kwargs):
---> 85 return getattr(self.get_queryset(), name)(*args, **kwargs)
86 manager_method.__name__ = method.__name__
87 manager_method.__doc__ = method.__doc__~/github/medium-clone/medium_clone/env/lib/python3.9/site-packages/django/db/models/query.py in get(self, *args, **kwargs)
427 return clone._result_cache[0]
428 if not num:
--> 429 raise self.model.DoesNotExist(
430 "%s matching query does not exist." %
431 self.model._meta.object_nameDoesNotExist: User matching query does not exist.
결론
표로 위의 내용을 정리하면 다음과 같다.
+------------------------------------------+
| setting | 저장 |
+------------------------------------------+
| None | User만 저장 |
| ATOMIC_REQUESTS True | 아무것도 저장 X |
| with transaction.atomic() | 아무것도 저장 X |
| @transaction.atomic | 아무것도 저장 X |
+------------------------------------------+
하나의 메서드, 뷰에서 두 개 이상의 인스턴스를 저장해야한다면 트랜잭션을 이용하면 좋을 것 같다. 보통 두 개 이상을 저장한다는 것은 서로 연관이 있기 때문에 한 번에 저정돼야한다는 것을 의미할 가능성이 크기 때문이다. 단, 성능 문제가 있을 수 있으니 트래픽이 많이 몰리는 앤드포인트에 쓰이는 메서드, 뷰는 조심하도록 하자.