Django에서 DB의 데이터 삽입, 수정 및 삭제를 진행할 때 성공과 실패가 분명하고 상호 독립적이며 일관되게끔 처리하는 기능을 어떻게 활용할 수 있을까, 트랜잭션의 개념부터 제대로 잡고 가자.
데이터베이스 트랜잭션(Database Transaction)은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미한다. 그리고 이 atomic이라는 개념은 아키텍쳐, 도메인 관점 등에서 굉장히 많이 사용되는 단어다. 핵심은 상호 독립적, 일관성 이다.
이론적으로 데이터베이스 시스템은 각각의 트랜잭션에 대해 원자성(Atomicity), 일관성(Consistency), 고립성(Isolation), 영구성(Durability)을 보장한다. 이 성질을 첫글자를 따 ACID라 부른다. 하지만 무조건적은 ACID 성질을 지키는 것은 오히려 DBMS의 퍼포먼스 저하를 유발할 수 있다. 그래서 완화 시켜서 사용하는 경우가 종종 있다.
즉, 트랜잭션은 데이터베이스에서 한꺼번에 수행되어야 할 일련의 연산들 이다. 전부 성공하거나 전부 실패되거나 둘 중 하나의 작업을 하며 트랙잭션의 모든 연산은 반드시 한꺼번에 완료가 되야 하며 그렇지 않은 경우에는 한꺼번에 취소되어야 하는 원자성을 가지고 있다.
완료가 되면 DBMS에서 COMMIT을 통해 작업 결과가 반영 되며, 문제가 발생한 경우는 ROLLBACK 을 호출 해 마지막 commit 전 까지 작업을 모두 취소하여 DB 자체에 영향을 미치지게 않게 할 수 있다.
하나의 트랜잭션이 더 작게 나눌 수 없는 최소의 단위라는 뜻이다. 트랜잭션이 모두 반영되거나, 아니면 전혀 반영되지 않아야 하는 특징을 나타낸다. 계좌이체를 하는 경우를 생각해보자. 송금하는 도중에 문제가 발생하여 돈을 받아야 하는 사람에게 제대로 전달되지 않았다. 하지만 돈을 보내는 사람에게서는 이미 돈이 빠져나갔다면 큰 문제가 발생하게 될 것이다. 계좌이체 도중 문제가 발생하게 되면 송금하기 이전상태를 유지하게 되는 것을 원자성이라 볼 수 있다.
쉽게 'all or nothing' 특성으로 설명된다
각각의 트랜잭션은 다른 트랜잭션의 수행에 영향을 받지 않고 독립적으로 수행되어야 한다.
트랜잭션이 수행되고 있을 때, 다른 트랜잭션의 연산작업이 중간에 끼어들어 기존 작업에 영향을 주지 못하도록 하는 것을 말한다. 독립성이 보장된다면 계좌 이체작업을 진행하고 있는 도중에 계좌의 잔액을 조회한다 거나 하는 작업을 동시에 수행할 수 없게 되는 것이다.
UPDATE SET seq = seq + 1 WHERE id = 1;
의 쿼리를 예로 들어, UNDO 작업을 하면, UPDATE SET seq = seq - 1 WHERE id = 1;
의 작업을 하는 것이다. 즉, 앞서 말한 UNDO 데이터 마저 없을 때, 마지막 기억 지점 부터 장애 시점까지 DB의 임시 저장값 (Buffer cache)을 복구하게 된다.
UPDATE SET seq = seq + 1 WHERE id = 1;
를 다시해서 UNDO 데이터를 만들고, 그 UNDO를 이용해 COMMIT되지 않은 데이터를 모두 ROLLBACK 함으로써 복구를 완료하게 된다. 결국 REDO가 UNDO를 복구하고 최종적으로 UNDO가 복구를 하게 된다.
django offical docs - https://docs.djangoproject.com/ko/4.0/topics/db/transactions/ 를 기반으로 합니다.
위에서 말한 DBMS의 트랜잭션 성질을 django에서도 지원한다. 바로 django.db.transaction
를 통해서 말이다. 우선 장고는 기본적으로 auto commit
을 기본값으로 채택한다. 즉 코드에 트랜잭션의 명시가 없으면 INSERT, UPDATE등의 sql query를 실행 후 바로 commit을 진행한다는 것이다.
만약 기본적으로 auto commit을 제공하지 않으면 사용자가 직접 commit 하거나 rollback을 진행해야 하는데 이러한 작업을 매번 한다고 생각하면 귀찮다. 그래서 장고에서는 기본값으로 auto commit을 지원해 성공 시, 자동으로 DB에 commit, 실패 시 자동으로 rollback을 처리해 준다.
atomic(using=None, savepoint=True, durable=False)
3번째 인자 durable은 장고 3.2 버전에서 추가된 기능이고, 이하의 버전에서는 해당 인자가 없이 using, savepoint 2가지의 인자만 사용한다.
보통 2개 이상의 쿼리를 실행시켜 이를 모두 성공 또는 실패(atomic)로 처리해야 한다면 atomic이란 기능을 사용하면 된다. 사용법은 데코레이터와 with문 2가지가 존재하며, 아래와 같다.
@transaction.atomic()
def update_user(user_id: int, updated_company_name: str):
Profile.objects.filter(user__id=user_id).update(company_name=updated_company_name)
User.objects.filter(id=user_id).update(company_name=updated_company_name)
with transaction.atomic():
Profile.objects.filter(user__id=user_id).update(company_name=updated_company_name)
User.objects.filter(id=user_id).update(company_name=updated_company_name)
우선 데코레이팅을 활용한 것은 FBV 기반의 request API 에 활용하기에 아주 적절하다.
두 가지 모두 기능은 동일하다. 만약 Profile 수정이 성공하고 User에서 실패가 난다면 성공한 Profile의 데이터도 롤백이 진행이 된다.
atomic의 첫 번째 인자인 using은 어떤 DB에 저장할 지 지정하는 인자이다. settings.py에 DB를 나열하는데 보통 default로 선언하지만 DB가 default1, default2 와 같이 여러개라면 atomic(using=default2)로 지정하면 default2 라고 선언한 DB에 저장할 수 있다.
from django.db import DatabaseError, transaction
obj = MyModel(active=False)
obj.active = True
try:
with transaction.atomic():
obj.save()
except DatabaseError:
obj.active = False
if obj.active:
...
with transaction.atomic():
내부에서 try - except을 사용하는 것을 django는 추천하지 않는다. 정확한 이유는 docs의 Avoid catching exceptions inside atomic!
부분에 자세하게 설명이 되어 있다.
그래서 위와 같은 구조를 추천하는데, 위 구조를 자세하게 한 번 살펴보자.
두 번째 인자는 savepoint를 허용할 지 인데 False로 설정하면 savepoint 생성을 비활성화 할 수 있다. savepoint를 허용하지 않아도 트랜잭션에 의해 무결성을 보장할 순 있지만 에러 핸들링이 멈추게 되므로 과도한 savepoint로 인한 오버헤드가 눈에 띄게 발생하지 않는다면 해당 옵션을 건들지 않는 것이 좋다.
with transaction.atomic():
Profile.objects.filter(user__id=user_id).update(company_name=updated_company_name)
transaction.commit()
User.objects.filter(id=user_id).update(company_name=updated_company_name)
a.save() # Succeeds, and never undone by savepoint rollback
sid = transaction.savepoint()
try:
b.save() # Could throw exception
transaction.savepoint_commit(sid)
except IntegrityError:
transaction.savepoint_rollback(sid)
c.save() # Succeeds, and a.save() is never undone
트랜잭션이 성공적으로 수행이 된 이후 호출되어야 하는 함수가 있다면 on_commit에 등록하면 된다. 인자로 전달된 함수는 트랜잭션이 성공적으로 commit이 된 직후, 호출이 되고 rollback이 되었다면 해당 함수는 삭제되어 호출되지 않는다.
만약 트랜잭션이 활성화 되지 않은 상태에서 on_commit에 함수를 등록하면 바로 실행이 된다.
from django.db import transaction
def do_something():
print("do_something")
# send a mail, invalidate a cache, fire off a Celery task, etc.
@transaction.atomic()
def test():
transaction.on_commit(do_something)
msg = Message(text="hello")
category.save()
위 코드를 실행시키면 Message object가 create (save)되고 -> commit 되면서 -> "do_something"이 출력된다. 만약 test()
함수에서 error가 발생되면 commit이 당연하게 안되기 때문에, do_something()
함수는 실행되지 못한다.
django에서는 이 on_commit을 활용해서 transaction.on_commit(lambda: some_celery_task.delay('arg1'))
와 같은 worker를 통한 작업도 예시로 든다.
django signal보다 더 simple하고, 작은 규모의 (즉 모든 모델 트렌잭션 대상으로 어떤 액션을 할 필요 없는) 후행 처리에 효과적인 것 같다.
def test():
first()
second()
@transaction.atomic()
def first():
Model1.save()
Model2.save()
@transaction.atomic()
def second():
Model3.save()
Model4.save()
@transaction.atomic()
def test():
first()
second()
def first():
Model1.save()
Model2.save()
def second():
Model3.save()
Model4.save()