데이터베이스 | Transaction(+ django @transaction.atomic 사용)

Jihun Kim·2022년 1월 8일
1

데이터베이스

목록 보기
2/7

@transaction을 공부하기 전에, 먼저 transaction의 올바른 정의가 무엇인지 알아야 할 필요가 있다.


Transaction이란 무엇인가?

트랜잭션은 데이터베이스의 상태를 변화시키는 하나의 논리적 기능을 수행하기 위한 작업의 단위 또는 한꺼번에 모두 수행되어야 하는 일련의 연산들을 의미한다.

복잡하게 설명되어 있지만 줄여서 기억해 보자면 데이터베이스의 상태를 변화시키는 작업 이라고 할 수 있을 것 같다.

하나의 트랜잭션은 커밋(commit) 되거나 롤백(rollback) 된다. 이 때, 하나의 트랜잭션이 안전하게 수행되기 위한 조건이 있는데 이를 ACID 라고 한다. 이는 Atomiticy(원자성) + Consistency(일관성) + Isolation(고립성, 독립성) + Durability(지속성) 의 약자이다.


ACID

원자성(Atomicity)

  • 원자성이란 하나의 트랜잭션을 더 이상 잘개 쪼갤 수 없는 최소한의 업무 단위
  • 트랜잭션은 데이터베이스에 모두 반영 되든지 아니면 전혀 반영되지 않아야 하는 All or Nothing 의 상태가 되어야 한다.
  • 작업이 부분적으로 실행 또는 중단되지 않는 것을 보장하며 트랜잭션 실행 도중 문제가 발생할 경우 중단이 아니라 모두 실패 또는 모두 성공 의 상태가 되어야 한다.
  • 가령, 100개의 명령어로 구성된 트랜잭션 중 99개가 완료 1개가 실패라면 이는 실패 로 간주된다.
  • 만약 트랜잭션 단위로 데이터가 처리되지 않으면 데이터 처리 시스템을 이해하기 힘들 뿐만 아니라 오작동 원인을 찾기가 어려워진다.

원자성은 어떻게 보장할까?

  • 트랜잭션에서 원자성은 수행하고 있는 트랜잭션에 의해 변경된 내용을 유지하면서 이전에 커밋된 상태를 임시 영역에 따로 저장함으로써 보장한다. 이 영역을 롤백 세그먼트 라고 하며 현재 수행하고 있는 트랜잭션에 의해 새롭게 변경되는 내역을 데이터베이스 테이블 이라 한다.
  • 오류가 발생하면 현재 내역을 날리고 롤백 세그먼트에 저장된 상태로 롤백을 한다.
  • 그런데 이 때, 트랜잭션의 길이가 길어지게 되면 확실하게 오류가 발생하지 않는 부분도 다시 처음부터 작업을 수행해야 하기 때문에, 확실한 부분에 대해서는 롤백 되지 않도록 세이브 포인트를 지정할 수 있다. → 그러면 이 포인트 이후부터 롤백을 진행하게 된다.

일관성(Consistency)

  • 트랜잭션이 완료된 결괏값이 일관적인 DB 상태를 유지하는 것을 말함
  • 시스템이 가지고 있는 고정 요소는 트랙잭션 수행 전과 후의 상태가 같아야 한다.
  • 트랜잭션이 진행되는 동안 데이터베이스가 변경되더라도 업데이트 된 데이터베이스로 트랜잭션이 진행되지 않고 처음 트랜잭션 진행했을 때의 데이터베이스를 참조하게 된다. → 이 덕분에 각 사용자가 일관된 데이터를 볼 수 있다.
  • 트랜잭션 수행시 보존되어야 하는 일관성 예시 → 기본 키, 외래 키와 같은 무결성 제약 조건들(명시적 일관성 조건): A 테이블의 pk를 fk로 사용하는 B 테이블이 있을 때, pk의 제약 조건이 변경되면 B 테이블에서도 pk가 변경되어야 한다. → 하나은행에서 국민은행으로 돈 이체할 때 두 계좌의 돈의 총합이 같아야 함(비명시적 일관성 조건)

고립성(Isolation)

  • 트랜잭션이 실행하는 도중에 변경한 데이터는 해당 트랜잭션이 완료될 때까지 다른 트랜잭션이 참조하지 못하게하는 특성이다.
  • 클라이언트는 같은 데이터를 공유하고 있기 때문에 트랜잭션은 동시에 진행되어야 하는데, 이 때 트랜잭션은 상호 간의 존재를 알지 못하는 상태로 독립적으로 진행 되어야 한다.
  • DBMS의 병행 제어 모듈 이 트랜잭션의 고립성을 보장한다.
  • 예를 들어, 하나의 트랜잭션이 A 계좌에서 작업하고 있을 때 다른 트랜잭션이 A 계좌에 대해 참조하거나 관여할 수 없으며 다른 트랜잭션은 해당 트랜잭션이 끝날 때까지 대기해야 한다.

고립성 보장

  • 한 트랜잭션이 데이터를 읽을 때 여러 다른 트랜잭션이 읽을 수는 있어야 하기 때문에 이를 허용하는 공유 록(shared_lock) 을 한다. 즉, 오직 읽기만 허용하는 상태이다.
  • 한 트랜잭션이 데이터를 쓸 때는 다른 트랜잭션이 읽을 수도 쓸 수도 없는 상태인 배타 록(exclusive_lock)을 한다.
  • 한 트랜잭션의 읽기 또는 쓰기 작업이 끝나면 데이터에 대한 잠금을 풀어 주기 위해 언락(unlock)을 통해 다른 트랜잭션이 록(lock)을 할 수 있도록 만들어 준다.

→ 이 때, 주의해야 할 점은 록과 언락을 잘못 사용하면 데드락(deadlock) 상태에 빠질 수 있다. 이는 모든 트랜잭션이 아무것도 수행할 수 없는 불능 상태가 되는 것이다.............!


지속성(Duration)

  • 트랜잭션의 성공 결과 값은 장애 발생 후에도 변함없이 보관되어야 한다. 트랜잭션이 정상적으로 완료된 경우 버퍼의 내용을 데이터베이스(하드디스크)에 확실하게 기록해야 하는데 DMBS가 책임지고 데이터베이스에 기록하는 성질을 지속성이라 한다.
  • 부분 완료된 경우에는 작업을 취소해야 한다.


트랜잭션의 상태

트랜잭션의 상태



연산

커밋

  • 모든 작업을 정상적으로 처리하겠다고 확정하는 명령어 → 처리 과정을 데이터베이스에 영구적으로 저장함
  • 커밋을 수행하면 하나의 트랜잭션 과정을 종료하는 것이 되며 이전 데이터가 완전히 업데이트 된다.

롤백

  • 트랜잭션 처리 과정 중 발생한 변경사항을 취소하라는 명령어로, 연산 작업 중 문제가 발생할 경우 실행한다.
  • 위에서 언급했듯이, 원자성을 구현하기 위해 해당 트랜잭션이 수행한 모든 연산이 취소 되며 트랜잭션이 시작하기 전의 상태(마지막 커밋 완료 시점)로 되돌린다(All or Nothing). → 이 때, 커밋 하여 저장한 것만 복구한다.


세이브 포인트

  • 만약 여러 개의 쿼리 실행을 수행하는 트랜잭션이라면 사용자가 트랜잭션 중간 단계에서 세이브 포인트를 지정할 수 있다.
  • 세이브 포인트를 통해 전체가 아닌 특정 부분에서 트랜잭션을 취소할 수 있다. → 이를 통해 트랜잭션을 작게 분할하는 것이 가능해진다.
  • 롤백을 실행하면 해당 세이브 포인트 지점까지 처리한 작업이 모두 롤백 된다.


이제 transaction 공부가 끝났으니 Django에서 transaction을 어떻게 사용하고 있는지 알아보자. 주로 @transaction 형태로 사용한다.


@transaction

장고는 default로 autocommit 모드로 동작한다. 따라서, 모든 쿼리는 트랜잭션이 활동 상태가 아닌 이상 데이터베이스로 즉시 커밋 된다. Django는 트랜잭션 또는 세이브 포인트를 자동으로 사용하여 여러 쿼리, 특히 delete() 및 update() 쿼리가 필요한 ORM 작업의 무결성을 보장 한다.


장고에서 autocommit을 사용하는 이유

SQL 쿼리는 트랜잭션이 이미 활성화 된 상태가 아니라면 트랜잭션을 시작하게 된다. 그 후, 이러한 트랜잭션은 커밋되거나 롤백 되어야 한다. 그러나, 이를 개발자가 일일이 해결하기에는 번거로움이 있기 때문에 대부분의 데이터베이스는 autocommit 모드를 제공한다. 따라서, autocommit이 켜져 있고 활성 트랜잭션이 없는 상태라면 각 SQL 쿼리는 하나의 트랜잭션으로 래핑 된다. 이 쿼리는 트랜잭션 시작 + 커밋/롤백을 자동으로 실행하게 되는 것이다.

현재 Python Database API Specification v2.0(PEP249)에 따르면 autocommit은 초기에는 비활성 된 상태여야 한다. 그러나 장고는 이를 오버라이드 해서 활성화 한 상태로 사용한다.


언제 @transaction을 사용해야 할까?

만약 한 api에서 복합적으로 데이터베이스 값을 저장하도록 하는 로직을 사용한다면 @transaction을 적용하거나 별도의 함수를 생성해서 오류 처리를 해야 한다. 그렇지 않으면 데이터베이스에 저장된 값을 신뢰할 수 없는 상태가 된다.



transaction 사용하기

atomic 메소드와 함께 사용한다. atomic을 이용하면 데이터베이스의 원자성이 보장되는 코드 블럭을 만들 수 있다. 원자성은 앞에서 언급했듯이 트랜잭션을 안전하게 수행하기 위한 기본 조건 중 하나이다.

부동산 사이트를 만든다고 가정해 보자!

@transaction.atomic

장고 공식문서에서 제공하는 기본 사용 방법은 다음과 같다.

from django.db import transaction

@transaction.atomic
def viewfunc(request):
	# This code executes inside a transaction.
	do_stuff()

위와 같이 하나의 코드 블럭인 viewfunc에 decorator로 @transaction.atomic을 사용할 수 있다. 그러면 만약 해당 코드 블럭 실행시 에러가 발생했을 경우 viewfunc 이전의 상태로 롤백될 수가 있다.

그런데 만약에, 해당 함수 내부에서 트랜잭션을 생성하고 싶다면 아래와 같이 context manager를 이용해야 한다.


transaction as a context manager

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()

이 방법을 이용하면 메소드 전체가 아니라 메소드의 일부분만 트랜잭션으로 묶어주게 된다. 그러면 우리는 트랜잭션으로 묶일 부분을 직접 지정해 주기만 하면 된다.


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()

위의 예시에서는, 만약 generate_relationships()가 무결성을 해하는 오류를 일으키더라도 add_children() 는 문제 없이 실행할 수 있게 된다. 또한 create_parent() 역시 같은 트랜잭션에 바운드 된 상태가 유지 된다.

generate_relationships() 가 오류를 일으키면 IntegrityError를 인식해 handle_exception() 함수가 실행되어 오류를 처리할 수 있게 된다.


savepoint를 직접 지정하기

savepoint와 commit 지점을 지정하여 오류가 발생하더라도 원하는 지점으로 롤백할 수 있다.

sid = transaction.savepoint()  # 세이브 포인트
try:
    obj.save()
    transaction.savepoint_commit(sid)  # 해당 시점의 세이브 포인트 커밋
except:
    transaction.savepoint_rollback(sid)  # 해당 시점의 세이브 포인트로 롤백

print("done!")

만약 부동산 매물 게시물을 작성하면서 동시에 부가 매물 정보를 수정해야 한다고 해보자. 그러면 이 때 게시물 수정은 성공했지만 부가 정보 수정시 오류가 생길 수도 있다(다른 방법으로 오류를 피할 수도 있겠지만 아무튼 그렇다고 가정한다.). 이를 해결하기 위해서는 savepoint를 지정하는 방법을 사용할 수 있는 것이다.

from django.db import transaction

def realestate_post_save(post_id, product_id):
	post = Post.objects.get(id=post_id)  # 게시물 아이디
	product = Product.objects.get(id=product_id)  # 매물 아이디
	new_title = request.data.get("new_title")  # 게시물 제목
	new_status = request.data.get("new_status")  # 매물 상태

	post.title = new_title  # 게시물 제목 수정
	product.status = new_status  # 매물 상태 수정

	post.save()

	sid = transactioin.savepoint()

	try:
		product.save()
		transaction.savepoint_commit(sid)
	except Exception:
		transaction.savepoint_rollback(sid)
		

장고는 만약 atomic() 블럭이 active 상태라면 트랜잭션의 원자성을 해칠 수 있기 때문에 커밋 또는 롤백하기를 거부한다는 점에 유의해야 한다.


commit후의 action 지정하기

만약 transaction이 성공적으로 commit 되었을 때 그 직후 실행해야 하는 어떤 action(이메일 알림, 캐시 invalidation 등등) on_commit() 메소드를 사용할 수 있다.

on_commit()의 인자로 함수를 넘길 수 있다. 단, 해당 함수는 인자를 필요로 하지 않아야 한다.

장고 공식 문서에 따르면 아래와 같이 사용이 가능하다.

on_commit(funcusing=None)

from django.db import transaction

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

transaction.on_commit(do_something)

이 때, lambda를 통해 익명 함수를 넘길 수도 있다. 가령 추첨 이벤트에서 10명의 당첨된 사용자에게 안내 알림을 보내야 하는 상황일 경우 아래와 같은 방법을 이용할 수 있다. 해당 예시는 8퍼센트 프로덕트 블로그에서 가져왔다.

with transaction.atomic():
    event.draw_lots(10)

    for user in event.won_users:
        transaction.on_commit(lambda user=user: send_notification(user))

이 경우 주의해야 하는 점은 won_users의 user 각각에게 알림을 보내야 한다는 점인데, 만약 lambda send_notification(user)(user=user: 부분이 없음) 이렇게 되어 있다면 1명의 user에게 10번의 안내 메일을 보내는 상황이 된다. 왜냐하면 콜백 함수에 매개변수가 없고 인자 user를 전달받지 못했기 때문이다.

그러나, lambda user=user: send_notification(user)를 사용하면 콜백함수는 user를 매개변수로 가지며 인자로 전달 받는 형태가 된다.

즉, 아래와 같은 형태가 된다.

# user를 매개변수로 가지며 user를 인자로 전달 받는 콜백함수
# lambda user=user: send_notification(user)
def call_back(user=user_1):
    send_notification(user)

# user를 매개변수로 갖지 않으며 user를 인자로 전달 받지 못하는 콜백함수
# lambda send_notification(user)
def call_back():
    send_notification(user)


참고

1) 위키해시

http://wiki.hash.kr/index.php/트랜잭션

2) 블로그

https://junghn.tistory.com/entry/DataBase기초-트랜잭션이란-무엇인가-Transaction

https://velog.io/@kho5420/Django-장고-트랜잭션-활용하기

https://blog.live2skull.kr/django/django-orm-02-transaction/

https://076923.github.io/posts/Python-Django-13/#django-transaction-구현

https://8percent.github.io/2020-10-26/transaction-on-commit/

3) 장고 공식문서

https://docs.djangoproject.com/en/4.0/topics/db/transactions/#autocommit-details

profile
쿄쿄

0개의 댓글