Search

Django transaction atomic(1) - usage

카테고리
Back-end
태그
Django
게시일
2024/05/28
수정일
2024/07/12 04:04
시리즈
Django & REST Framework
1 more property

1. Intro

Transaction이란 데이터베이스의 상태를 변화시키기 위해 수행하는 작업 단위입니다. 이를 좀 더 풀어서 설명하면 작업의 단위 또는 한 번에 모두 수행되어야 하는 일련의 연산을 의미합니다. 트랜잭션은 데이터베이스 상태를 변화시키는 작업의 상태에 따라 커밋(commit) 되거나 잘못됐다면 롤백(rollback) 될 수 있습니다. 또한 이런 통신이 안전하게 수행되기 위한 조건인 ACID(Atomicity, Consistency, Isolation, Durability)가 있습니다.
이번 포스트에서는 Database Transaction의 ACID와 함께 django 내에서 이러한 transaction을 컨트롤하기 위해 어떤 방법을 활용할 수 있을지 방법과 예시를 적어보려고 합니다.

2. ACID

ACID는 트랜잭션이 안전하게 수행되기 위한 조건입니다. 각각의 요소가 어떤 것이 있는지 알아보겠습니다.

Atomicity - 원자성

하나의 트랜잭션을 더 이상 쪼갤 수 없는 최소한의 업무 단위 (원자라는 단어처럼, 더 쪼갤 수 없다는 비유)
트랜잭션은 데이터베이스에 모두 반영되거나 전혀 반영되지 않아야 함 - All or Nothing의 상태
작업이 부분적으로 실행 또는 중단되지 않는 것을 보장하며, 만약 문제가 발생할 경우 전체 반영 여부를 결정
원자성을 보장하기 위해서 변경된 내용에 대해 유지하면서 이전에 커밋된 상태를 임시 영역에 따로 저장하여 보관합니다. 이 영역을 rollback segment(롤백 세그먼트)라 합니다. 그리고 트랜잭션이 새롭게 변경되는 영역은 database table(데이터베이스 테이블)이라 합니다.
하지만 트랜잭션 길이가 길어지게 되면 확실하게 오류가 발생하지 않는 부분도 다시 처음부터 작업을 수행해야할 수 있습니다. 때문에 이런 부분은 별도로 롤백되지 않도록 하기 위해 savepoint를 지정할 수 있습니다. savepoint 부분에서 commit을 한다면, commit한 savepoint 이후 부터 롤백을 진행하게 됩니다.

Consistency - 일관성

트랜잭션이 완료된 결과값은 일관된 DB 상태를 유지해야 함
시스템이 가지고 있는 고정 요소는 트랜잭션 수행 전/후 동일해야 함
트랜잭션 중 DB의 상태가 변경되더라도, 트랜잭션 시작 시점의 DB 상태를 기준을 참조하게 되어, 각각의 사용자는 일관된 데이터를 볼 수 있음

Isolation - 고립성

트랜잭션이 실행되는 중 변경한 데이터는 해당 트랜잭션이 완료될 때까지 다른 트랜잭션이 참조하지 못함
클라이언트는 같은 데이터를 공유하기 때문에 트랜잭션은 동시에 진행되어야 하지만, 상호간의 존재를 알지 못하는 상태로 독립적으로 진행되어야 함
하나의 트랜잭션이 참조하고 있으면, 다른 트랜잭션이 해당 데이터에 참조/관여할 수 없으며, 트랜잭션이 종료될 때까지 대기해야 함
한 트랜잭션이 데이터를 읽을 때 여러 다른 트랜잭션이 읽을 수는 있어야 하기 때문에 shared_lock 을 적용합니다. 다만, 이때는 읽기만 가능합니다. 만약 쓰기 상태일 때는 다른 트랜잭션이 읽기/쓰기 모두 불가능한 exclusive_lock 을 합니다. 트랜잭션의 읽기, 쓰기 작업이 끝나면 데이터에 대한 잠금을 해제하고(unlock) 다른 트랜잭션이 lock을 할 수 있도록 합니다.
만약 lock, unlock을 잘못하용하게 되면 deadlock 상태에 빠질 수 있어 트랜잭션이 불가능한 상태가 될 수 있습니다.

Durability - 지속성

트랜잭션 성공 값은 장애가 발생하더라도 변함없이 보관돼야 함
부분 완료된 경우 작업을 취소해야 함

3. Transaction in django

Django에서는 기본적으로 autocommit 모드로 동작합니다. 따라서, 모든 쿼리는 트랜잭션이 활성 상태가 아니라면 즉시 DB에 커밋되는 구조입니다. Django는 트랜잭션 또는 세이브 포인트를 자동으로 사용하여 여러 쿼리, 특히 delete(), update() 쿼리가 필요한 ORM 작업의 무결성을 제공합니다.

Use case

주로 Transaction은 복합적으로 데이터베이스 값을 저장하도록 하는 로직을 사용할 경우 활용할 수 있습니다. 별도의 함수를 생성해서 오류를 처리하거나, 그렇지 않을 경우 DB에 저장된 값을 신뢰할 수 없는 상태가 됩니다.

Usage - Decorator

Django 공식 문서에서 Transaction을 사용하는 방법은 다음과 예시로 나타나 있습니다.
from django.db import transaction @transaction.atomic def viewfunc(request): # This code executes inside a transaction. do_stuff()
Python
복사
위와 같이 함수 위에 decorator를 선언하여 사용할 수 있으며, viewfunc 내에서 에러가 발생할 경우 viewfunc 이전의 상태로 롤백될 수 있습니다. 만약 함수 내부에서 트랜잭션을 생성하고 싶을 경우에는 context manager를 이용해야 합니다.

Usage - Context manager

Context manager에 대한 설명은 아래의 python docs를 참고하면 될 것 같습니다.
from django.db import transaction def viewfunc(request): # This code executes in autocommit mode (Django's default). do_stuff() with transaction.atomic(): # This code executes inside a transaction. do_more_stuff()
Python
복사
위와 같은 방법을 활용하면 메소드 전체가 아닌, 메소드의 일부분의 트랜잭션만 묶어줄 수 있습니다.

Try - Except

만약 Try Except를 사용하고 싶다면, 공식 문서에서는 다음과 같이 사용하라고 권장합니다.
from django.db import IntegrityError, transaction @transaction.atomic def viewfunc(request): create_parent() try: with transaction.atomic(): generate_relationships() except IntegrityError: handle_exception() add_children()
Python
복사
이 이유에 대한 원문을 보면 다음과 같습니다.
When exiting an atomic block, Django looks at whether it’s exited normally or with an exception to determine whether to commit or roll back. If you catch and handle exceptions inside an atomic block, you may hide from Django the fact that a problem has happened. This can result in unexpected behavior. ...
Plain Text
복사
원문의 내용에서 핵심만 요약하면, try - except 내에 atomic 블록을 사용하게 되면, Django 입장에서 에러가 발생했는지 여부를 알 수 없게 되어, transaction에 문제가 발생할 수 있다는 걸 이야기합니다. 때문에 atomic 블록을 사용하고자 할 때는 try - except 내에 사용하는 것을 권장합니다.

Performing actions after commit

만약 트랜잭션이 성공적으로 commit됐을 경우, 다음으로 실행돼야 할 팜수를 지정해줄 수 있습니다. 이때 사용할 수 있는 메소드는 on_commit() 입니다.
에제는 아래와 같이 사용이 가능합니다.

Partial 예제

from functools import partial for user in users: transaction.on_commit(partial(send_invite_email, user=user))
Python
복사

Lambda 예제

for user in users: transaction.on_commit(lambda user=user: send_invite_email(user))
Python
복사

5. Conclusion

Django 에서 트랜잭션을 컨트롤하는 방법에 대해 알아보았습니다. 다음 포스트에서는 여러 개의 Database를 활용할 대 transaction.atomic을 활용하는 방법과 예시에 대해 알아보겠습니다.