웹 프로그래밍/DRF

효율적인 Django 사용을 위한 몇가지 (분산트랜잭션 처리와 ACID)

728x90
반응형

박살난 디비성적과는 별개로 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)
728x90
반응형