ABOUT ME

-

10-07 22:15
Today
-
Yesterday
-
Total
-
  • [Django] transaction
    언어/파이썬 & 장고 2021. 7. 24. 18:20

    트랜잭션이란

    트랜잭션은 DB의 데이터 삽입, 수정 및 삭제를 진행할 때 성공과 실패가 분명하고 상호 독립적이며 일관되게끔 처리하는 기능입니다. 트랜잭션에 대해선 https://brownbears.tistory.com/181 에서 자세하게 설명하고 있습니다.

    Django의 transaction

    장고에서도 transaction을 제공해주고 있습니다. django.db.transaction 파일에 존재하며 사용은 아주 간단합니다. 사용법 설명에 앞서 장고는 auto commit을 기본값으로 제공하고 있습니다. 즉, 코드에 트랜잭션이라고 명시가 안되어 있으면 insert, update와 같은 문장을 바로 DB에 commit을 진행합니다.

    장고에서 auto 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 test():
        Student.objects.filter(id=1).update(name='Kim')
        Member.objects.filter(name='Kim').update(mileage=10000)

    with문

    with transaction.atomic():
        Student.objects.filter(id=1).update(name='Kim')
        Member.objects.filter(name='Kim').update(mileage=10000)

    두 가지 모두 기능은 동일합니다. 만약 Student에 수정이 성공하고 Member에서 실패가 난다면 성공한 Student의 데이터도 롤백이 진행됩니다.

    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:
        ...

    위 예시에서 Dango의 트랜잭션 관리는 다음과 같습니다.

    1. 가장 바깥쪽 atomic을 들어갈 때 트랜잭션 활성화
    2. 내부 atomic 블록에 들어갈 때, savepoint 생성
    3. 내부 블록을 종료할 때, savepoint 해제 (성공했을 경우)하거나 rollback (에러가 발생했을 경우) 진행
    4. try-except 블록을 빠져 나갈 때, 트랜잭션을 commit (성공했을 경우) 하거나 rollback(에러가 발생했을 경우) 진행

    두 번째 인자는 savepoint를 허용할 지 인데 False로 설정하면 savepoint 생성을 비활성화 할 수 있습니다. savepoint를 허용하지 않아도 트랜잭션에 의해 무결성을 보장할 순 있지만 에러 핸들링이 멈추게 되므로 과도한 savepoint로 인한 오버헤드가 눈에 띄게 발생하지 않는다면 해당 옵션을 건들지 않는 것이 좋습니다.

    위 예시와 같이 만약 with문을 사용해 transction.atomic()을 사용했는데 try-except를 사용한다면 try-except를 with문보다 외부에 작성해야 합니다. 만약 with문 내부에 try-except를 작성했다면 에러 발생 시, rollback을 제대로 수행하지 못하게 됩니다. transaction.atomic() 내부에서 에러가 발생하면 connection.rollback의 값이 True로 변경되고 이 값을 보고 rollback이 진행되는데 내부의 try-except로 인해 rollback이 제대로 이뤄지지 않고 해당 값은 계속해서 True로 남게 되어 나머지 모든 transaction들이 TransactionManagementError를 발생하게 됩니다. (uwsgi worker가 고장)

    commit(using=None) - 수동 commit

    해당 함수를 호출하면 수동으로 현재까지 작업된 내용을 DB에 commit합니다. 이후, 해당 블럭의 결과와 상관없이 트랜잭션이 종료됩니다.

    with transaction.atomic():
        Student.objects.filter(id=1).update(name='Kim')
        transaction.commit()
        Member.objects.filter(name='Kim').update(mileage=10000)

    rollback(using=None) - 수동 rollback

    해당 함수를 호출하면 수동으로 현재까지 작업된 내용을 rollback합니다. 이후, 해당 블럭의 결과와 상관없이 트랜잭션이 종료됩니다.

    savepoint 수동 관리

    savepoint를 수동으로 생성, commit, rollback할 수 있습니다.

    savepoint(using=None)

    함수를 사용해 수동으로 savepoint를 생성할 수 있습니다. savepoint를 생성하면 sid를 반환합니다.

    savepoint_commit(sid, using=None)

    수동으로 생성 후, 반환된 sid를 필수로 받아 해당 시점을 commit 할 수 있습니다.

    savepoint_rollback(sid, using=None)

    수동으로 생성 후, 반환된 sid를 필수로 받아 해당 시점을 rollback 할 수 있습니다.

    clean_savepoints(using=None)

    지금까지 생성된 savepoint를 전부 지웁니다.

    savepoint를 수동으로 관리하는 기능이니 정확하게 설계가 된 코드가 아닌 이상 사용하지 않는 것이 좋아보입니다.

    sid = transaction.savepoint()
    try:
        Student.objects.filter(id=1).update(name='Kim')
        Member.objects.filter(name='Kim').update(mileage=10000)
        transaction.savepoint_commit(sid)
    except:
        transaction.savepoint_rollback(sid)
    
    transaction.clean_savepoints()

    on_commit(func, using=None) - commit 이후 작업 수행

    트랜잭션이 성공적으로 수행이 된 이후 호출되어야 하는 함수가 있다면 on_commit에 등록하면 됩니다. 인자로 전달된 함수는 트랜잭션이 성공적으로 commit이 된 직후, 호출이 되고 rollback이 되었다면 해당 함수는 삭제되어 호출되지 않습니다.

    만약 트랜잭션이 활성화 되지 않은 상태에서 on_commit에 함수를 등록하면 바로 실행이 됩니다.

    def done():
        print('done')
    
    @transaction.atomic()
    def test():
        transaction.on_commit(done)
        category = Category(category='Men')
        category.save()
    
    test()
    
    # done

    아래는 강제로 에러를 발생시켰는데 이 경우, 호출되지 않습니다.

    def done():
        print('done')
    
    @transaction.atomic()
    def test():
        transaction.on_commit(done)
        category = GsCategory(category_code='1', gs_category_code='1', )
        category.save()
        raise ValueError
    
    test()

    활성화된 트랜잭션이 없다면 바로 호출이 됩니다. (save() 이후 에러가 발생해도 트랜잭션이 없으므로 가장 먼저 호출됩니다.)

    def done():
        print('done')
    
    def test():
        transaction.on_commit(done)
        category = GsCategory(category_code='1', gs_category_code='1', )
        category.save()
    
    test()
    
    # done

    트랜잭션 테스트

    위의 예시는 1개의 함수 내에서만 처리하는 예시였는데 다른 함수를 호출하는 형식일 때, 어떻게 처리되는지 테스트를 진행해 봅니다.

    def test():
        first()
        second()
    
    @transaction.atomic()
    def first():
        Model1.save()
        Model2.save()
    
    @transaction.atomic()
    def second():
        Model3.save()
        Model4.save()

    위와 같이 작성이 된 경우, first() 함수는 성공적으로 진행이 되었는데 second()에서 에러가 발생할 때, first()와 second()는 별개로 본다면 상관이 없지만 test() 함수 내 모든 작업이 성공하거나 실패해야 된다고 하면 문제가 발생합니다. 따라서 아래처럼 test() 함수에도 트랜잭션 데코레이터나 with문을 선언해야 second() 함수에서 에러가 발생했을 경우, first() 함수도 롤백이 진행되게 됩니다.

    @transaction.atomic()
    def test():
        first()
        second()
    
    @transaction.atomic()
    def first():
        Model1.save()
        Model2.save()
    
    @transaction.atomic()
    def second():
        Model3.save()
        Model4.save()

    아래는 test()함수에만 트랜잭션이 선언되어 있고 first()와 second()에는 없는 케이스입니다.

    @transaction.atomic()
    def test():
        first()
        second()
    
    def first():
        Model1.save()
        Model2.save()
    
    def second():
        Model3.save()
        Model4.save()
        raise ValueError
    
    test()

    이 경우는 모든 케이스가 성공해야만 저장이 되고 에러가 발생하면 모두 rollback이 됩니다. 즉, 트랜잭션의 원자성이 보장됩니다.

    다음은 데코레이터 내부에 try-except를 추가한 테스트입니다.

    @transaction.atomic()
    def test():
        first()
        second()
    
    @transaction.atomic()
    def first():
        Model1.save()
        Model2.save()
    
    @transaction.atomic()
    def second():
        try:
            Model3.save()
            Model4.save()
            raise ValueError
        except Exception as e:
            print(e)
    test()
    Model5.save()

    위 케이스는 second() 에서 에러를 강제로 발생시켰지만 except에서 잡아 아무 처리를 하지 않아 model1, model2, model3, model4, model5 모두 저장이 정상적으로 이뤄지는 것을 확인했습니다.

    Django의 트랜잭션은 사용하기 쉽지만 각 함수나 메소드를 호출하는 부분에서는 고려해야되는 사항이 있으므로 주의하야 사용해야 합니다.

    참고자료

    https://docs.djangoproject.com/en/3.2/topics/db/transactions/

    https://lee-seul.github.io/django/2019/02/02/django-transactionmanagementerror.html

    댓글 0