이번글은 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)
ModelVIewSet 과 ModelSerializer 로 간단하게 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의 동작에 차이가 있기 때문이다.
이 글(PostgreSQL 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
복사
User의 money 를 업데이트는 정상적으로 되었지만, 그 이후 작업에서 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
복사
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
복사
User의 money 를 업데이트를 진행하면서 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
복사