@transaction
을 공부하기 전에, 먼저 transaction의 올바른 정의가 무엇인지 알아야 할 필요가 있다.
트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 하는 일련의 연산들을 의미한다.
복잡하게 설명되어 있지만 줄여서 기억해 보자면 데이터베이스의 상태를 변화시키는 작업
이라고 할 수 있을 것 같다.
하나의 트랜잭션은 커밋(commit) 되거나 롤백(rollback) 된다. 이 때, 하나의 트랜잭션이 안전하게 수행되기 위한 조건이 있는데 이를 ACID
라고 한다. 이는 Atomiticy(원자성) + Consistency(일관성) + Isolation(고립성, 독립성) + Durability(지속성) 의 약자이다.
All or Nothing
의 상태가 되어야 한다.모두 실패
또는 모두 성공
의 상태가 되어야 한다.실패
로 간주된다.원자성은 어떻게 보장할까?
롤백 세그먼트
라고 하며 현재 수행하고 있는 트랜잭션에 의해 새롭게 변경되는 내역을 데이터베이스 테이블
이라 한다.세이브 포인트
를 지정할 수 있다. → 그러면 이 포인트 이후부터 롤백을 진행하게 된다.병행 제어 모듈
이 트랜잭션의 고립성을 보장한다.고립성 보장
공유 록(shared_lock)
을 한다. 즉, 오직 읽기만 허용하는 상태이다.배타 록(exclusive_lock)
을 한다.언락(unlock)
을 통해 다른 트랜잭션이 록(lock)
을 할 수 있도록 만들어 준다.→ 이 때, 주의해야 할 점은 록과 언락을 잘못 사용하면 데드락(deadlock)
상태에 빠질 수 있다. 이는 모든 트랜잭션이 아무것도 수행할 수 없는 불능 상태가 되는 것이다.............!
이제 transaction 공부가 끝났으니 Django에서 transaction을 어떻게 사용하고 있는지 알아보자. 주로 @transaction
형태로 사용한다.
장고는 default로 autocommit 모드로 동작한다. 따라서, 모든 쿼리는 트랜잭션이 활동 상태가 아닌 이상 데이터베이스로 즉시 커밋 된다. Django는 트랜잭션 또는 세이브 포인트를 자동으로 사용하여 여러 쿼리, 특히 delete() 및 update() 쿼리가 필요한 ORM 작업의 무결성을 보장 한다.
SQL 쿼리는 트랜잭션이 이미 활성화 된 상태가 아니라면 트랜잭션을 시작하게 된다. 그 후, 이러한 트랜잭션은 커밋되거나 롤백 되어야 한다. 그러나, 이를 개발자가 일일이 해결하기에는 번거로움이 있기 때문에 대부분의 데이터베이스는 autocommit 모드를 제공한다. 따라서, autocommit이 켜져 있고 활성 트랜잭션이 없는 상태라면 각 SQL 쿼리는 하나의 트랜잭션으로 래핑 된다. 이 쿼리는 트랜잭션 시작 + 커밋/롤백을 자동으로 실행하게 되는 것이다.
현재 Python Database API Specification v2.0(PEP249)에 따르면 autocommit은 초기에는 비활성 된 상태여야 한다. 그러나 장고는 이를 오버라이드 해서 활성화 한 상태로 사용한다.
만약 한 api에서 복합적으로 데이터베이스 값을 저장하도록 하는 로직을 사용한다면 @transaction
을 적용하거나 별도의 함수를 생성해서 오류 처리를 해야 한다. 그렇지 않으면 데이터베이스에 저장된 값을 신뢰할 수 없는 상태가 된다.
atomic
메소드와 함께 사용한다. atomic
을 이용하면 데이터베이스의 원자성이 보장되는 코드 블럭을 만들 수 있다. 원자성은 앞에서 언급했듯이 트랜잭션을 안전하게 수행하기 위한 기본 조건 중 하나이다.
부동산 사이트를 만든다고 가정해 보자!
장고 공식문서에서 제공하는 기본 사용 방법은 다음과 같다.
from django.db import transaction
@transaction.atomic
def viewfunc(request):
# This code executes inside a transaction.
do_stuff()
위와 같이 하나의 코드 블럭인 viewfunc
에 decorator로 @transaction.atomic
을 사용할 수 있다. 그러면 만약 해당 코드 블럭 실행시 에러가 발생했을 경우 viewfunc
이전의 상태로 롤백될 수가 있다.
그런데 만약에, 해당 함수 내부에서 트랜잭션을 생성하고 싶다면 아래와 같이 context manager
를 이용해야 한다.
from django.db import transaction
def viewfunc(request):
# This code executes in autocommit mode (Django's default).
do_stuff()
with transaction.atomic():
# This code executes inside a transaction.
do_more_stuff()
이 방법을 이용하면 메소드 전체가 아니라 메소드의 일부분만 트랜잭션으로 묶어주게 된다. 그러면 우리는 트랜잭션으로 묶일 부분을 직접 지정해 주기만 하면 된다.
from django.db import IntegrityError, transaction
@transaction.atomic
def viewfunc(request):
create_parent()
try:
with transaction.atomic():
generate_relationships()
except IntegrityError:
handle_exception()
add_children()
위의 예시에서는, 만약 generate_relationships()
가 무결성을 해하는 오류를 일으키더라도 add_children()
는 문제 없이 실행할 수 있게 된다. 또한 create_parent()
역시 같은 트랜잭션에 바운드 된 상태가 유지 된다.
generate_relationships()
가 오류를 일으키면 IntegrityError
를 인식해 handle_exception()
함수가 실행되어 오류를 처리할 수 있게 된다.
savepoint와 commit 지점을 지정하여 오류가 발생하더라도 원하는 지점으로 롤백할 수 있다.
sid = transaction.savepoint() # 세이브 포인트
try:
obj.save()
transaction.savepoint_commit(sid) # 해당 시점의 세이브 포인트 커밋
except:
transaction.savepoint_rollback(sid) # 해당 시점의 세이브 포인트로 롤백
print("done!")
만약 부동산 매물 게시물을 작성하면서 동시에 부가 매물 정보를 수정해야 한다고 해보자. 그러면 이 때 게시물 수정은 성공했지만 부가 정보 수정시 오류가 생길 수도 있다(다른 방법으로 오류를 피할 수도 있겠지만 아무튼 그렇다고 가정한다.). 이를 해결하기 위해서는 savepoint
를 지정하는 방법을 사용할 수 있는 것이다.
from django.db import transaction
def realestate_post_save(post_id, product_id):
post = Post.objects.get(id=post_id) # 게시물 아이디
product = Product.objects.get(id=product_id) # 매물 아이디
new_title = request.data.get("new_title") # 게시물 제목
new_status = request.data.get("new_status") # 매물 상태
post.title = new_title # 게시물 제목 수정
product.status = new_status # 매물 상태 수정
post.save()
sid = transactioin.savepoint()
try:
product.save()
transaction.savepoint_commit(sid)
except Exception:
transaction.savepoint_rollback(sid)
장고는 만약 atomic()
블럭이 active 상태라면 트랜잭션의 원자성을 해칠 수 있기 때문에 커밋 또는 롤백하기를 거부한다는 점에 유의해야 한다.
만약 transaction이 성공적으로 commit 되었을 때 그 직후 실행해야 하는 어떤 action(이메일 알림, 캐시 invalidation 등등) on_commit()
메소드를 사용할 수 있다.
on_commit()
의 인자로 함수를 넘길 수 있다. 단, 해당 함수는 인자를 필요로 하지 않아야 한다.
장고 공식 문서에 따르면 아래와 같이 사용이 가능하다.
on_commit
(func, using=None)
from django.db import transaction
def do_something():
pass # send a mail, invalidate a cache, fire off a Celery task, etc.
transaction.on_commit(do_something)
이 때, lambda
를 통해 익명 함수를 넘길 수도 있다. 가령 추첨 이벤트에서 10명의 당첨된 사용자에게 안내 알림을 보내야 하는 상황일 경우 아래와 같은 방법을 이용할 수 있다. 해당 예시는 8퍼센트 프로덕트 블로그에서 가져왔다.
with transaction.atomic():
event.draw_lots(10)
for user in event.won_users:
transaction.on_commit(lambda user=user: send_notification(user))
이 경우 주의해야 하는 점은 won_users의 user 각각에게 알림을 보내야 한다는 점인데, 만약 lambda send_notification(user)
(user=user: 부분이 없음) 이렇게 되어 있다면 1명의 user에게 10번의 안내 메일을 보내는 상황이 된다. 왜냐하면 콜백 함수에 매개변수가 없고 인자 user를 전달받지 못했기 때문이다.
그러나, lambda user=user: send_notification(user)
를 사용하면 콜백함수는 user를 매개변수로 가지며 인자로 전달 받는 형태가 된다.
즉, 아래와 같은 형태가 된다.
# user를 매개변수로 가지며 user를 인자로 전달 받는 콜백함수
# lambda user=user: send_notification(user)
def call_back(user=user_1):
send_notification(user)
# user를 매개변수로 갖지 않으며 user를 인자로 전달 받지 못하는 콜백함수
# lambda send_notification(user)
def call_back():
send_notification(user)
참고
1) 위키해시
http://wiki.hash.kr/index.php/트랜잭션
2) 블로그
https://junghn.tistory.com/entry/DataBase기초-트랜잭션이란-무엇인가-Transaction
https://velog.io/@kho5420/Django-장고-트랜잭션-활용하기
https://blog.live2skull.kr/django/django-orm-02-transaction/
https://076923.github.io/posts/Python-Django-13/#django-transaction-구현
https://8percent.github.io/2020-10-26/transaction-on-commit/
3) 장고 공식문서
https://docs.djangoproject.com/en/4.0/topics/db/transactions/#autocommit-details