Django Model - 트랜잭션, DB Transaction, Atomic

정현우·2022년 7월 19일
3

Django Basic to Advanced

목록 보기
24/40

Django Model - Transaction, DB Atomic

Django에서 DB의 데이터 삽입, 수정 및 삭제를 진행할 때 성공과 실패가 분명하고 상호 독립적이며 일관되게끔 처리하는 기능을 어떻게 활용할 수 있을까, 트랜잭션의 개념부터 제대로 잡고 가자.

데이터베이스 트랜잭션(Database Transaction)

  • 데이터베이스 트랜잭션(Database Transaction)은 데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위다. 여기서 유사한 시스템이란 트랜잭션이 성공과 실패가 분명하고 상호 독립적이며, 일관되고 믿을 수 있는 시스템을 의미한다. 그리고 이 atomic이라는 개념은 아키텍쳐, 도메인 관점 등에서 굉장히 많이 사용되는 단어다. 핵심은 상호 독립적, 일관성 이다.

  • 이론적으로 데이터베이스 시스템은 각각의 트랜잭션에 대해 원자성(Atomicity), 일관성(Consistency), 고립성(Isolation), 영구성(Durability)을 보장한다. 이 성질을 첫글자를 따 ACID라 부른다. 하지만 무조건적은 ACID 성질을 지키는 것은 오히려 DBMS의 퍼포먼스 저하를 유발할 수 있다. 그래서 완화 시켜서 사용하는 경우가 종종 있다.

  • 즉, 트랜잭션은 데이터베이스에서 한꺼번에 수행되어야 할 일련의 연산들 이다. 전부 성공하거나 전부 실패되거나 둘 중 하나의 작업을 하며 트랙잭션의 모든 연산은 반드시 한꺼번에 완료가 되야 하며 그렇지 않은 경우에는 한꺼번에 취소되어야 하는 원자성을 가지고 있다.

  • 완료가 되면 DBMS에서 COMMIT을 통해 작업 결과가 반영 되며, 문제가 발생한 경우는 ROLLBACK 을 호출 해 마지막 commit 전 까지 작업을 모두 취소하여 DB 자체에 영향을 미치지게 않게 할 수 있다.

트랜잭션 성질

1. 원자성(Atomicity)

  • 하나의 트랜잭션이 더 작게 나눌 수 없는 최소의 단위라는 뜻이다. 트랜잭션이 모두 반영되거나, 아니면 전혀 반영되지 않아야 하는 특징을 나타낸다. 계좌이체를 하는 경우를 생각해보자. 송금하는 도중에 문제가 발생하여 돈을 받아야 하는 사람에게 제대로 전달되지 않았다. 하지만 돈을 보내는 사람에게서는 이미 돈이 빠져나갔다면 큰 문제가 발생하게 될 것이다. 계좌이체 도중 문제가 발생하게 되면 송금하기 이전상태를 유지하게 되는 것을 원자성이라 볼 수 있다.

  • 쉽게 'all or nothing' 특성으로 설명된다

2. 일관성(Consistency)

  • 일관성은 트랜잭션이 완료된 결괏값이 일관적인 데이터베이스 상태를 유지하는 것을 말한다. 사실 이 말만으로는 잘 이해가 되지 않는다. 예를 들면, 고객 정보가 담겨있는 데이터베이스에 새로운 고객이 등록되면 그 데이터베이스를 참조하는 다른 하위 계층의 데이터베이스도 같은 고객의 세부 정보를 가져와야 한다. 또는 모든 계좌의 돈은 0원 이상이어야 한다. 라는 무결성 제약이 있다면 이를 위반하는 트랜잭션은 모두 중단되어야 한다.

3. 독립성, 격리성(Isolation)

  • 각각의 트랜잭션은 다른 트랜잭션의 수행에 영향을 받지 않고 독립적으로 수행되어야 한다.

  • 트랜잭션이 수행되고 있을 때, 다른 트랜잭션의 연산작업이 중간에 끼어들어 기존 작업에 영향을 주지 못하도록 하는 것을 말한다. 독립성이 보장된다면 계좌 이체작업을 진행하고 있는 도중에 계좌의 잔액을 조회한다 거나 하는 작업을 동시에 수행할 수 없게 되는 것이다.

4. 영속성, 지속성(Durability)

  • 트랜잭션을 성공한 후 데이터베이스에 반영된 것은 영원히 반영되어야 한다는 것을 의미한다. 시스템에 문제가 발생하거나 종료되더라도 데이터베이스에 반영된 값은 그대로 유지되어야 한다. 은행에 시스템이 마비되었다가 재개되었을 때 계좌의 금액이 바뀌게 된다면 큰 문제가 될 것이다.

트랜잭션의 상태

1. Active(활동 상태)

  • 트랜잭션이 수행을 시작하여 현재 수행 중인 상태

2. Partially Committed(부분 완료 상태)

  • 마지막 연산이 실행된 직후의 상태. 연산은 모두 처리했지만, 데이터베이스에 반영되지 않았다.

3. Commited(완료 상태)

  • 트랜잭션이 성공적으로 완료되어 Commit을 한 상태, 즉 반영이 모두 된 상태.

4. Failed(실패 상태)

  • 장애가 발생하여 트랜잭션이 중단된 상태

5. Aborted(철회 상태)

  • 수행에 실패하여 Rollback 연산을 실행한 상태

UNDO, REDO

UNDO는 반대로 작업을 진행하는 것

  • UPDATE SET seq = seq + 1 WHERE id = 1; 의 쿼리를 예로 들어, UNDO 작업을 하면, UPDATE SET seq = seq - 1 WHERE id = 1; 의 작업을 하는 것이다.
  • UNDO는 아래 데이터들을 같이 기록한다.
    1. INSERT 시, insert 된 로우의 rowid 기록
    2. UPDATE 시, 바뀐 컬럼의 바뀌기 전 값 기록
    3. DELETE 시, 지워진 모든 데이터 기록
  • 복구는 UNDO를 통해 ROLLBACK을 한다. 하지만 시스템 장애의 경우 이 "UNDO"에 대한 데이터 마저 모두 날라간다.

REDO는 마지막 CHECK POINT부터 장애까지의 DB BUFFER CACHE 를 복구

  • 즉, 앞서 말한 UNDO 데이터 마저 없을 때, 마지막 기억 지점 부터 장애 시점까지 DB의 임시 저장값 (Buffer cache)을 복구하게 된다.

  • UPDATE SET seq = seq + 1 WHERE id = 1; 를 다시해서 UNDO 데이터를 만들고, 그 UNDO를 이용해 COMMIT되지 않은 데이터를 모두 ROLLBACK 함으로써 복구를 완료하게 된다. 결국 REDO가 UNDO를 복구하고 최종적으로 UNDO가 복구를 하게 된다.


Django에서 transaction

django offical docs - https://docs.djangoproject.com/ko/4.0/topics/db/transactions/ 를 기반으로 합니다.

django.db.transaction

  • 위에서 말한 DBMS의 트랜잭션 성질을 django에서도 지원한다. 바로 django.db.transaction 를 통해서 말이다. 우선 장고는 기본적으로 auto commit을 기본값으로 채택한다. 즉 코드에 트랜잭션의 명시가 없으면 INSERT, UPDATE등의 sql query를 실행 후 바로 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 update_user(user_id: int, updated_company_name: str):
    Profile.objects.filter(user__id=user_id).update(company_name=updated_company_name)  
	User.objects.filter(id=user_id).update(company_name=updated_company_name)


with transaction.atomic():
   	Profile.objects.filter(user__id=user_id).update(company_name=updated_company_name)  
	User.objects.filter(id=user_id).update(company_name=updated_company_name)
  • 우선 데코레이팅을 활용한 것은 FBV 기반의 request API 에 활용하기에 아주 적절하다.

  • 두 가지 모두 기능은 동일하다. 만약 Profile 수정이 성공하고 User에서 실패가 난다면 성공한 Profile의 데이터도 롤백이 진행이 된다.

  • 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:
	...
  • with transaction.atomic(): 내부에서 try - except을 사용하는 것을 django는 추천하지 않는다. 정확한 이유는 docs의 Avoid catching exceptions inside atomic! 부분에 자세하게 설명이 되어 있다.

  • 그래서 위와 같은 구조를 추천하는데, 위 구조를 자세하게 한 번 살펴보자.

    1. 가장 바깥쪽 atomic을 들어갈 때 트랜잭션 활성화
    2. 내부 atomic 블록에 들어갈 때, savepoint 생성
    3. 내부 블록을 종료할 때, savepoint 해제 (성공했을 경우)하거나 rollback (에러가 발생했을 경우) 진행
    4. try-except 블록을 빠져 나갈 때, 성공했을 경우 트랜잭션을 commit 하거나 에러가 발생했을 경우 rollback 진행
  • 두 번째 인자는 savepoint를 허용할 지 인데 False로 설정하면 savepoint 생성을 비활성화 할 수 있다. savepoint를 허용하지 않아도 트랜잭션에 의해 무결성을 보장할 순 있지만 에러 핸들링이 멈추게 되므로 과도한 savepoint로 인한 오버헤드가 눈에 띄게 발생하지 않는다면 해당 옵션을 건들지 않는 것이 좋다.

commit(using=None) - 수동 commit

  • 해당 함수를 호출하면 수동으로 현재까지 작업된 내용을 DB에 commit한다. 이후, 해당 블럭의 결과와 상관없이 트랜잭션이 종료된다.
with transaction.atomic():
   	Profile.objects.filter(user__id=user_id).update(company_name=updated_company_name)  
    transaction.commit()
	User.objects.filter(id=user_id).update(company_name=updated_company_name)

rollback(using=None) - 수동 rollback

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

savepoint 수동 관리

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

savepoint(using=None)

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

savepoint_commit(sid, using=None)

  • 수동으로 생성 후, 반환된 sid를 필수로 받아 해당 시점을 commit 할 수 있습니다.
  • sid는 save point의 고유 id 값이다.

savepoint_rollback(sid, using=None)

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

clean_savepoints(using=None)

  • 지금까지 생성된 savepoint를 전부 지운다.
a.save() # Succeeds, and never undone by savepoint rollback
sid = transaction.savepoint()
try:
    b.save() # Could throw exception
    transaction.savepoint_commit(sid)
except IntegrityError:
    transaction.savepoint_rollback(sid)
c.save() # Succeeds, and a.save() is never undone
  • savepoint를 활용하여 error를 catch해 rollback하는 예시는 위와 같다.

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

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

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

from django.db import transaction

def do_something():
    print("do_something")
    # send a mail, invalidate a cache, fire off a Celery task, etc.

@transaction.atomic()
def test():
	transaction.on_commit(do_something)
	msg = Message(text="hello")
    category.save()
  • 위 코드를 실행시키면 Message object가 create (save)되고 -> commit 되면서 -> "do_something"이 출력된다. 만약 test() 함수에서 error가 발생되면 commit이 당연하게 안되기 때문에, do_something() 함수는 실행되지 못한다.

  • django에서는 이 on_commit을 활용해서 transaction.on_commit(lambda: some_celery_task.delay('arg1')) 와 같은 worker를 통한 작업도 예시로 든다.

  • django signal보다 더 simple하고, 작은 규모의 (즉 모든 모델 트렌잭션 대상으로 어떤 액션을 할 필요 없는) 후행 처리에 효과적인 것 같다.

다양한 묶음 트랜잭션을 모두 원자성 보장하려면?

def test():
    first()
    second()

@transaction.atomic()
def first():
    Model1.save()
    Model2.save()

@transaction.atomic()
def second():
    Model3.save()
    Model4.save()
  • first, second는 각 각 원자성을 보장하지만, test는 원자성을 보장하지 않는다. 즉 first는 되고, second는 터지는 경우가 발생할 수 있는 것이다.
@transaction.atomic()
def test():
    first()
    second()

def first():
    Model1.save()
    Model2.save()

def second():
    Model3.save()
    Model4.save()
  • 이런 경우는 위와 같이 parent (가장 상단의 transaction)에 atomic을 걸어주면 된다.

출처

profile
도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결” 에 몰두하는 개발자가 되고싶습니다. 그러기 위해 항상 새로운 것에 도전하고 노력하는 개발자가 되고 싶습니다!

0개의 댓글