백엔드 개발을 하면 언젠가는 트랜잭션과 마주하게 된다.
장고에서 트랜잭션을 어떻게 관리하는지에 대해 알아보자.
급하다면 6. Django ORM의 트랜잭션
부터 ...
트랜잭션은 DB의 상태를 변화시키기 위해 수행하는 작업의 단위이다.
SQL을 이용한 CRUD가 그 작업들이다.
하지만 하나의 쿼리가 하나의 트랜잭션인 것은 아니며, 여러 쿼리를 묶어 하나의 트랜잭션으로 처리할 수 있다.
아래와 같은 상황을 가정을 해보겠다.
서비스 내 재화를 소비하여 물품을 구매하는 로직을 구현해야 한다.
1.cash
테이블에서 잔액을 조회한다.
2. 재화를 소비하고 잔액을update
한다.
3. 구매 내역을 생성한다.
4. 소비 이후 잔액을 조회한다.
위와 같은 로직을 구현했을 때 아래와 같은 순서로 쿼리가 발생한다.
(실제 서비스라면 더 많은 쿼리들이 발생하겠지만, 예시이니 간단하게만)
이 때 발생할 수 있는 문제들을 생각해보면,
재화를 소비하는 로직이 동시에 발생하면 어떡하지?
잔액 부족이나 특정 이유로 재화를 소비할 수 없는 상황이 발생하면 어떡하지?
잔액과 구매 내역이 같지 않으면 어떡하지?
...
문제가 발생했을 때 어떻게 되돌리지...
아래와 같이 하나의 트랜잭션으로 처리하면 가능하다.
하지만 어떻게?
트랜잭션의 특성, commit
과 rollback
에 대해 먼저 간단히 알아보겠다.
트랜잭션은 아래 4가지 특성을 가진다.
원자성 (Actomicity)
트랜잭션의 수행 결과는 전부 반영되거나, 전부 반영되지 않아야 한다.
일관성 (Consistency)
트랜잭션 작업의 결과는 항상 일관되어야 한다.
독립성 (Isolation)
각각의 트랜잭션은 서로 간섭할 수 없이 독립적이다.
영구성 (Durability)
트랜잭션이 성공적으로 처리되면 결과는 영구적으로 반영되어야 한다.
혹시 감이 오는지?
이번 포스트에서는 원자성, 영구성에 대해 다룬다.
트랜잭션이 종료될 때 아래와 같은 연산들을 한다.
commit
트랜잭션이 성공적으로 끝나서 결과에 대해 영구성을 보장하도록 한다.
rollback
트랜잭션이 원자성을 보장할 수 없을 때 수행하여 재시도하거나 되돌릴 수 있도록 한다.
성공적으로 수행되면 commit
하고, 문제가 발생하면 rollback
하여 상황을 해결할 수 있다!
너무 간단한데...
Django
에서는 어떨까?
Django
는 기본적으로 autocommit
이다.
각 ORM(쿼리)가 실행되는 즉시 DB에 commit
을 해버리는 방식이다.
명시적으로 트랜잭션을 제어하기 위해선 atomic
을 사용해야 한다.
이름에서부터 알 수 있듯이 atomic
을 사용해 원자성을 보장하는 코드 블록을 만들 수 있다.
atomic
으로 묶여있는 코드 블록은 하나의 트랜잭션으로 동작한다.
해당 코드 블록이 성공적으로 수행되면 commit
, 그렇지 않으면 rollback
한다.
atomic
을 사용하여 1장에서 정의된 상황에 하나의 요구사항을 추가해 간단하게 구현해보겠다.
서비스 내 재화를 소비하여 물품을 구매하는 로직을 구현해야 한다.
1.cash
테이블에서 잔액을 조회한다.
2. 재화를 소비하고 잔액을update
한다.
3. 상품이 구매 가능한 상태라면 구매 내역을 생성한다.
4. 그렇지 않다면 구매를 취소한다.
5. 구매 이후 잔액을 조회한다.
from django.db import transaction
def 잔액_확인(유저):
return Cash.objects.get(유저=유저).잔액
def 구매내역_생성(유저, 상품):
if 상품.구매_가능:
purchase_history = PurchaseHistory.objects.create(
유저=유저,
상품=상품
)
return purhcase_history
raise Exception(f"현재 {상품.이름}을 구매할 수 없습니다.")
@transaction.atomic
def 구매(유저, 상품):
잔액 = 잔액_확인(유저)
if 잔액 < 상품.가격:
raise Exception("잔액이 부족합니다.")
재화_소비(상품.가격)
구매내역_생성(상품)
return 잔액_확인(유저)
먼저 현재 잔액이 부족한 경우와 상품이 구매가 가능하지 않은 경우 의도적으로 raise
를 해주었다.
그렇다면 트랜잭션은 rollback
되고 유저의 잔액은 정상적으로 유지가 될 것이다.
문제 없이 마지막 코드까지 실행되면 결과는 commit
되어 생성된 구매내역과 잔액은 영구성이 보장 될 것이다.
atomic
은 decorator
이외에도 with
와 함께 사용할 수 있다.
def 구매(유저, 상품):
with transaction.atomic():
잔액 = 잔액_확인(유저)
if 잔액 < 상품.가격:
raise Exception("잔액이 부족합니다.")
재화_소비(상품.가격)
구매내역_생성(상품)
return 잔액_확인(유저)
상황에 맞게 사용하면 되겠지만, with
를 사용하면 indent
가 늘어나 나는 decorator
를 선호한다.
트랜잭션이 정상적으로 commit
될 때 수행할 내용도 지정할 수 있다.
구매에 성공하면 유저에게 구매 완료 이메일을 발송하는 셀러리 태스크를 호출한다고 해보자.
from functools import partial
def 구매(유저, 상품):
with transaction.atomic():
잔액 = 잔액_확인(유저)
if 잔액 < 상품.가격:
raise Exception("잔액이 부족합니다.")
재화_소비(상품.가격)
구매내역_생성(상품)
transaction.on_commit(partial(구매_완료_이메일_발송.delay, 유저.이메일))
return 잔액_확인(유저)
아... 초간단.
쉽다.
다음번엔 lock에 대해서 다뤄볼까 한다.
자세한 내용은 https://docs.djangoproject.com/en/4.1/topics/db/transactions/ 에서 확인 가능하다.