박살난 디비성적과는 별개로 ACID에 대한 기억은 남아있다. 다시 정리해보면,
Atomic: 원자성의 보장이다. 트랜잭션 내의 모든 작업이 성공적으로 완료되거나, 하나라도 실패하면 전체가 롤백돼야한다.
Consistency: 트랜잭션이 완료되면 데이터베이스는 항상 일관된 상태를 유지해야한다.
Isolation: 하나의 트랜잭션이 완료될 때까지 다른 트랜잭션이 영향을 받지 않도록 해야한다.
Durability: 트랜잭션이 성공적으로 완료되면, 시스템 장애가 발생해도 변경 사항이 유지돼야한다.
분산트랜잭션 처리에 대한 예시들을 찾아보던 중 적절하게 문제가 있는 한가지 예시를 발견하여 가져와 정리해보려한다.
from django.db import transaction
@transaction.atomic
def transfer_funds(sender, receiver, amount):
sender_acc = Account.objects.select_for_update().get(pk=sender)
receiver_acc = Account.objects.select_for_update().get(pk=receiver)
if sender_acc.balance < amount:
raise InsufficientFundsError
sender_acc.balance -= amount
receiver_acc.balance += amount
sender_acc.save()
receiver_acc.save()
TransactionRecord.objects.create(
sender=sender_acc,
receiver=receiver_acc,
amount=amount
)
@transaction.atomic과 select_for_update()의 사용에서 ACID 준수를 위한 노력이 엿보인다. @transaction.atomic를 사용함으로서 트랜잭션 실패시에 롤백이 되도록 설정하였고, select_for_update()를 사용해 두개 계좌에 락을 걸어 동시에 여러 트랜잭션이 수정하지 못하도록 방지를 했다.
1. Race Condition 발생 가능성 미고려
하지만 문제가 되는 부분은 save()를 개별호출하는 부분이다. 위 코드에서는 잔고를 증/감시키는 연산을 파이썬 메모리에서 수행한 이후에 save()를 호출한다. 이 과정에서 발생할 수 있는 race condition이 고려되지 않았다. 다른 트랜잭션이 같은 데이터를 변경할 수 있다는거다. 만약 두개의 다른 프로세스가 같은 sender_acc 객체를 가져와서 balance를 변경하면 마지막 save() 호출이 앞에 연산을 덮어씌울 것이다. 이건 F()로 해결할 수 있다. 데이터베이스 내에서 직접 연산을 수행하는거다.
2. 불필요 DB 연산
select_for_update()로 행을 잠그고 save()를 개별호출하면서 UPDATE query를 두번 실행한다. 과정을 톺아보면 SELECT -> 파이썬 연산 -> UPDATE 니까 필요없는 SELECT 연산이 발생한다. 이것도 F()로 해결할 수 있다. 디비내에서 직접 연산하므로 SELECT없이 UPDATE만 수행하면 된다.
3. TransactionRecord 생성 시점 문제(데이터 정합성)
TransactionRecord가 잔액 업데이트 이후에 저장이 된다. 중간에 충돌이 생기면 기록이 없어질 수 있다. 잔액 업데이트는 성공했는데 트랜잭션 기록은 저장되지 않을 수 있는 것이다. 트랜잭션이 다 성공하고 기록을 저장하되, refresh_from_db()로 업데이트된 계좌 정보를 다시 불러와서 반영하면 된다.
4. 개선된 코드
from django.db import transaction
from django.db.models import F
@transaction.atomic
def transfer_funds(sender, receiver, amount):
sender_acc = Account.objects.select_for_update().get(pk=sender)
if sender_acc.balance < amount:
raise InsufficientFundsError("잔액이 부족합니다.")
updated_rows = Account.objects.filter(pk=sender, balance__gte=amount).update(balance=F('balance') - amount)
if updated_rows == 0:
raise InsufficientFundsError("잔액이 부족합니다.")
Account.objects.filter(pk=receiver).update(balance=F('balance') + amount)
sender_acc.refresh_from_db()
receiver_acc = Account.objects.get(pk=receiver)
TransactionRecord.objects.create(sender=sender_acc, receiver=receiver_acc, amount=amount)
'웹 프로그래밍 > DRF' 카테고리의 다른 글
효율적인 Django 사용을 위한 몇가지(쿼리 최적화 적용편) (0) | 2025.02.26 |
---|---|
효율적인 Django 사용을 위한 몇가지(정규화 설계 적용편) (0) | 2025.02.26 |