Search

Django 트랜잭션 (2)

이번글은 Atomic을 사용했을 때 어떤 동작이 진행되는지 한번 알아볼 것이다.

Atomic → SQL

과연 Atomic을 사용한 코드는 실제 SQL 쿼리가 어떻게 생성될까? Django로 예시를 만들며 알아보자
[ 구성 환경 ]
Django: 4.2.15
DB: Postgresql 12.11 [ PostgreSQL Logging 설정 ]

예시 코드(Model)

최대한 간단하게 작성하였다. 다르게 작성해서 만들어봐도 무방하다.
from django.db import models class User(models.Model): name = models.CharField(max_length=100) age = models.PositiveIntegerField() money = models.PositiveIntegerField(default=0)
Python
복사

예시 코드(View)

ModelVIewSetModelSerializer 로 간단하게 API를 만들었다.
class TransactionTestView(ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer def get(self, request, pk=None): user = self.get_object() serializer = self.get_serializer(user) return Response({'data': serializer.data}) def create(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) with transaction.atomic(): user = serializer.create(serializer.validated_data) return Response({'data': user.id}) def update(self, request, pk=None): user = self.get_object() serializer = self.get_serializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) with transaction.atomic(): user = serializer.update(user, serializer.validated_data) # Nested 트랜잭션 사용 with transaction.atomic(): user.money = random.randint(1, 100000) user.save() return Response({'data': user.id})
Python
복사
class UserSerializer(ModelSerializer): class Meta: model = User fields = '__all__'
Python
복사

트랜잭션 정상 동작

트랜잭션이 정상적으로 동작할 때 경우이다.

단일 트랜잭션 (정상 처리)

사용자 생성을 위한 create 메서드 내(POST)에 User Model object 생성에 사용되는 코드를 Atomic 블럭을 사용한 트랜잭션으로 설정하였다.
class TransactionTestView(ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer ... def create(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) with transaction.atomic(): user = serializer.create(serializer.validated_data) ...
Python
복사
BEGIN - COMMIT 을 통해 트랜잭션의 작업이 반영되며, 내용은 아주 간단하다.
# 트랜잭션 시작 2024-09-03 02:20:22.053 KST [4231] LOG: statement: BEGIN # 데이터 추가 2024-09-03 02:20:22.056 KST [4231] LOG: statement: INSERT INTO "users_user" ("name", "age", "money") VALUES ('user1', 27, 0) RETURNING "users_user"."id" # 트랜잭션 반영 2024-09-03 02:20:22.060 KST [4231] LOG: statement: COMMIT
Plain Text
복사

MySQL vs PostgreSQL의 쿼리문 차이

이 글을 보던 분들 중 MySQL을 쓰시던 분들은 약간 의문이 있을 수 있다. MySQL의 쿼리 실행 내용이 약간 다르기 때문이다.
아래 로그가 완전히 동일하진 않겠지만, MySQL이라면 SET autocommit=0 / SET autocommit=1로 시작과 끝을 장식하기 때문이다.
정확하게는 START TRANSACTION(BEGIN) 구문을 사용한다.
하지만, 아래 공식 문서의 내용과 같이 MYSQL은 자동 커밋(autocommit)이 활성화 된 상태로 동작하기 때문에 위 구문으로 해당 모드를 비활성화 하는 것이다.
# Django ORM을 사용하며 로깅된 쿼리문이 `START TRANSACTION(BEGIN)` 이 아니지만, 둘 다 AutoCommit을 비활성화 하는 동일한 작업을 하는 것이다. SET autocommit=0 INSERT INTO "users_user" ("name", "age", "money") VALUES ('user1', 27, 0) RETURNING "users_user"."id" COMMIT SET autocommit=1
SQL
복사
이는 두 DBMS의 동작에 차이가 있기 때문이다.
이 글(link iconPostgreSQL DocumentationBEGIN)을 참고하면, PostgreSQL은 “기본적으로(BEGIN 없이) PostgreSQL은 "자동 커밋" 모드로 트랜잭션을 실행” 한다고 작성되어 있다.
즉, 트랜잭션 처리 시 굳이 AutoCommit 모드를 바꾸지 않고 PostgreSQL 자체가 자동 커밋을 하지 않는 문법을 별도로 사용하는 것이다.

Nested 트랜잭션 (정상 처리)

그렇다면, Nested 트랜잭션을 사용하도록 작성한 사용자 정보 수정 API를 사용해보자.
class TransactionTestView(ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer ... def update(self, request, pk=None): user = self.get_object() serializer = self.get_serializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) with transaction.atomic(): user = serializer.update(user, serializer.validated_data) with transaction.atomic(): user.money = random.randint(1, 100000) user.save() ...
Python
복사
신기한 점을 발견했다.
내부에 중첩된 Atomic 블럭은 별도의 BEGIN 이 아닌 SAVEPOINT 로 처리된다.
이는 중첩된 Atomic 블럭으로 관리되는 트랜잭션이 상위 트랜잭션에 영향을 받음을 알 수 있는 내용이다.
2024-09-03 02:22:54.988 KST [4277] LOG: statement: SELECT "users_user"."id", "users_user"."name", "users_user"."age", "users_user"."money" FROM "users_user" WHERE "users_user"."id" = 39 LIMIT 21 2024-09-03 02:22:54.994 KST [4277] LOG: statement: BEGIN 2024-09-03 02:22:54.997 KST [4277] LOG: statement: UPDATE "users_user" SET "name" = 'user1', "age" = 50, "money" = 0 WHERE "users_user"."id" = 39 2024-09-03 02:22:55.010 KST [4277] LOG: statement: SAVEPOINT "s6200160256_x1" 2024-09-03 02:22:55.017 KST [4277] LOG: statement: UPDATE "users_user" SET "name" = 'user1', "age" = 50, "money" = 97082 WHERE "users_user"."id" = 39 2024-09-03 02:22:55.020 KST [4277] LOG: statement: RELEASE SAVEPOINT "s6200160256_x1" 2024-09-03 02:22:55.022 KST [4277] LOG: statement: COMMIT
Plain Text
복사

Raise 발생을 통한 ROLLBACK 테스트

이번에는 강제로 Raise를 발생시켜 ROLLBACK을 하였을 때 어떻게 동작하는지 확인해보자

단일 트랜잭션 (ROLLBACK 처리)

트랜잭션 내 Raise를 발생시켜보자.
class TransactionTestView(ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer ... def create(self, request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) with transaction.atomic(): user = serializer.create(serializer.validated_data) raise Exception('ERROR WHEN USER CREATE') ...
Python
복사
Server에서도 정상적으로 에러 발생이 확인되었다.
DB에서도 ROLLBACK 을 통해 정상적으로 롤백 처리를 하였다.
2024-09-03 02:42:41.593 KST [4627] LOG: statement: BEGIN 2024-09-03 02:42:41.608 KST [4627] LOG: statement: INSERT INTO "users_user" ("name", "age", "money") VALUES ('user2', 27, 0) RETURNING "users_user"."id" 2024-09-03 02:42:41.615 KST [4627] LOG: statement: ROLLBACK
Plain Text
복사

Nested 트랜잭션 (최상단 트랜잭션 Raise 발생으로 ROLLBACK 처리)

우선 Nested된 트랜잭션 중 최상단 트랜잭션에서 에러가 생기는 경우를 확인해보자.
class TransactionTestView(ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer ... def update(self, request, pk=None): user = self.get_object() serializer = self.get_serializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) with transaction.atomic(): user = serializer.update(user, serializer.validated_data) with transaction.atomic(): user.money = random.randint(1, 100000) user.save() raise Exception('ERROR WHEN COMPLETE NESTED TRANSACTION') ...
Python
복사
Usermoney 를 업데이트는 정상적으로 되었지만, 그 이후 작업에서 Raise가 발생하는 경우였다.
Nested된 Atomic 블럭은 Savepoint로 설정 & RELEASE SAVEPOINT 를 통해 정상적으로 처리 되었다.
→ 하지만 이게 실제 반영되었음을 의미하는게 아니다.
money 의 업데이트를 위해 Nested Atomic Block으로 SAVEPOINT "s6189559808_x1" 를 설정했지만, Nested Atomic Block 내부의 작업은 정상적으로 처리되었기에 RELEASE SAVEPOINT 를 통해 해당 Savepoint를 해제하는 것이다.
이후 Raise 발생으로 ROLLBACK 동작하며 작업이 복원된다.
2024-09-03 02:46:04.882 KST [4689] LOG: statement: SELECT "users_user"."id", "users_user"."name", "users_user"."age", "users_user"."money" FROM "users_user" WHERE "users_user"."id" = 39 LIMIT 21 2024-09-03 02:46:04.889 KST [4689] LOG: statement: BEGIN 2024-09-03 02:46:04.896 KST [4689] LOG: statement: UPDATE "users_user" SET "name" = 'user1', "age" = 50, "money" = 97082 WHERE "users_user"."id" = 39 2024-09-03 02:46:04.899 KST [4689] LOG: statement: SAVEPOINT "s6189559808_x1" 2024-09-03 02:46:04.903 KST [4689] LOG: statement: UPDATE "users_user" SET "name" = 'user1', "age" = 50, "money" = 25964 WHERE "users_user"."id" = 39 2024-09-03 02:46:04.907 KST [4689] LOG: statement: RELEASE SAVEPOINT "s6189559808_x1" 2024-09-03 02:46:04.910 KST [4689] LOG: statement: ROLLBACK
Plain Text
복사
RELEASE SAVEPOINT 내용은 이 글(link iconPostgreSQL DocumentationRELEASE SAVEPOINT)을 참고해보자

Nested 트랜잭션 (중첩 트랜잭션 내부 Raise 발생으로 ROLLBACK 처리)

이제 Nested된 트랜잭션 내부에서 에러가 발생하는 경우를 확인해보자.
class TransactionTestView(ModelViewSet): queryset = User.objects.all() serializer_class = UserSerializer ... def update(self, request, pk=None): user = self.get_object() serializer = self.get_serializer(data=request.data, partial=True) serializer.is_valid(raise_exception=True) with transaction.atomic(): user = serializer.update(user, serializer.validated_data) with transaction.atomic(): user.money = random.randint(1, 100000) user.save() raise Exception('ERROR WHEN USER MONEY UPDATE') ...
Python
복사
Usermoney 를 업데이트를 진행하면서 Raise가 발생하는 경우이다.
Nested된 Atomic 블럭은 Savepoint로 설정 & ROLLBACK TO SAVEPOINT 으로 Nested Atomic 블럭의 트랜잭션 롤백 → RELEASE SAVEPOINT 로 Savepoint 해제
이후 상위 트랜잭션으로 전파되며, depth 별로 트랜잭션을 ROLLBACK한다.
2024-09-03 02:54:58.915 KST [4841] LOG: statement: SELECT "users_user"."id", "users_user"."name", "users_user"."age", "users_user"."money" FROM "users_user" WHERE "users_user"."id" = 39 LIMIT 21 2024-09-03 02:54:58.921 KST [4841] LOG: statement: BEGIN 2024-09-03 02:54:58.937 KST [4841] LOG: statement: UPDATE "users_user" SET "name" = 'user1', "age" = 50, "money" = 97082 WHERE "users_user"."id" = 39 2024-09-03 02:54:58.942 KST [4841] LOG: statement: SAVEPOINT "s6157250560_x1" 2024-09-03 02:54:58.945 KST [4841] LOG: statement: UPDATE "users_user" SET "name" = 'user1', "age" = 50, "money" = 7885 WHERE "users_user"."id" = 39 2024-09-03 02:54:58.949 KST [4841] LOG: statement: ROLLBACK TO SAVEPOINT "s6157250560_x1" 2024-09-03 02:54:58.952 KST [4841] LOG: statement: RELEASE SAVEPOINT "s6157250560_x1" 2024-09-03 02:54:58.955 KST [4841] LOG: statement: ROLLBACK
Plain Text
복사
RELEASE SAVEPOINT 내용은 이 글(link iconPostgreSQL DocumentationRELEASE SAVEPOINT)을 참고해보자

참고