[Django]DB Transaction 사용 시 주의할 점

David Im·2022년 8월 7일
0

본 글은 야간모드에 최적화 되어있습니다. 우측 상단에서 해 혹은 달모양을 클릭시어 velog 설정을 야간모드로 해주시면 더욱 편안하게 읽으실 수 있습니다.

회사에서 업무를 진행하다가 DB Transaction에 대해서 궁금한게 생겼다.
처음에 이 코드를 사용했을때는 워낙에 바빴닥 보니 사수분께서 그냥
'원자성보장을 위해서 DB 입출력이 동작하는 부분에서는 항상 써주어야한다' 라고 하셨던 기억이 난다.

그래서 이후로 나는 항상 C,U,D가 일어나는 과정에서는 해당 코드를 포함시켰었다.

우리는 멀티DB도 아니었고, 단순한 모놀리틱서버에 DB도 하나를 사용하고있었기때문에

뒤에오는 옵션들에 대해서는 그다지 중요하게 여기지 않아도 된다고 하셨다.

Django에서는 DB Transaction에 대한 원자성 보장을 아래 코드를 활용해서 사용한다.


with transaction.atomic(using=None, savepoint=True, durable=False)

그러다가 외부 업체 SI를 진행했던 기간동안에 해당 업체는 멀티 DB 구성을 사용하게 되었는데,

이때 Transaction 에 대한 궁금증이 생겼던 것 같다. 무엇보다 SI 업체에서 일을 진행할 때 Transaction atomic을 먹였을 때 Transaction이 제대로 동작하지 않았던 것이 가장 큰 궁금증이었다.

조금은 늦었지만, 여유가 생긴 지금에 와서야 나름대로 알아보고 공부했던 Transaction에 대해서 글을 작성해보고자 한다.

Transaction이란?

Transaction의 정의

트랜잭션은 DB의 데이터 삽입, 수정 및 삭제를 진행할 때 성공과 실패가 분명하고 상호 독립적이며 일관되게끔 처리하는 기능이다.

Transaction 특징

트랜잭션의 특징은 줄여서 ACID라고 부른다. 가장 기본적인 내용중의 하나이면서 대학교 DB개론시간에 배웠던 내용이지만, 이번 기회에 다시 한번 찾아보면서 정리했다. 예시로는 가장 대표적인 예인 금융 예를 참고했다.

  1. 원자성 : Atomicity

    • 트랜잭션의 연산은 데이터베이스에 모두 반영되든지 아니면 전부 반영되지 않아야한다.
    • 트랜잭션의 모든 명령은 반드시 전부 성공해야하고, 하나라도 오류가 발생하면 일련의 트랜잭션 과정이 모두 취소된다.

    👉 트랜잭션의 결과는 0아니면 1이어야한다는 소리. 계좌이체를 시도하여 송금했으나, 중간에 불특정한 사유로 실패한 경우, 계좌이체는 실패하고 송금한 금액은 원래 본인에게 돌아가야하며, 받는 대상의 계좌 총액 역시 변동이 없어야한다.

  1. 일관성 : Consistency

    • 트랜잭션이 성공적으로 수행되면 그 데이터는 언제나 일관성있는 데이터베이스 상태로 변환된다.
    • 시스템이 가진 고정요소의 경우, 트랜잭션 완료 전, 후의 상태가 동일해야한다.

    👉 트랜잭션이 일어난 이후의 데이터베이스는 데이터베이스의 제약이나 규칙을 만족해야한다는 말.
    예를 들어, '이름은 필수값' 이라는 제약사항을 가진 사용자 DB에 대해서 Trasnaction을 가동시켰을 때, 사용자를 삭제하고 이름 없는 사용자를 추가하게 될 경우 트랜잭션 이후에 '이름은 필수값' 이라는 제약사항을 위반한 것이 되어 일관성을 위반한 것이 된다.

  2. 독립성(격리성) : Isolation

    • 둘 이상의 트랜잭션이 동시에 같이 실행될 때, 하나의 트랜잭션이 실행되는 사이로 다른 트랜잭션의 연산이 영향을 줄 수 없다.
    • 수행중인 트랜잭션이 완전히 끝날 때 까지, 해당 결과에 대해서는 다른 트랜잭션은 참조 할 수 없다.

    👉 예를들어, A 계좌와 B 계좌로 각각 100만원씩 동시에 송금한다고 했을 경우, A 계좌 먼저 100만원, 그 이후에 B 계좌로 100만원을 송금한 것과 같은 결과를 내야한다. 각 트랜잭션은 철저하게 독립적이고 서로에게 간섭할 수 없기 때문이다.

  3. 영속성(지속성) : Durability

    • 성공적으로 완료된 트랜잭션의 결과는 시스템 오류가 발생하더라도, 영구적으로 반영되어야한다.

    👉 예를 들어 은행에서 게좌이체를 성공적으로 실행한 뒤에, 해당 은행 데이터베이스에 오류가 발생해 종료되더라도 계좌이체 내역은 기록으로 남아야 한다.


Django transaction.atomic 사용시 주의점

트랜잭션의 성질에 대해서 다시금 확인하고 정리했으니, Django 내에서 트랜잭션이 제대로 동작하지 않던 원인과 그 해결 방법에 대해서 남겨보도록 하겠다.

의심가는 부분은 크게 두가지였다.

  • 멀티 DB를 사용하는 상황이었는데 Trnsaction atomic을 두 DB에 전부 걸어주지 않아서 발생하는 문제
  • 자주쓰이는 함수(시간계산 함수, 인증을 위한 서버간 통신 호출 함수 등)를 공통 함수로 빼서 사용하고 있었는데, 그 함수들 내부에 try-catch 문이 들어가있어 트랜잭션이 제대로 동작하지 않는경우

각각의 의심가는 원인들에 대해서 테스트를 진행하고, 해결법을 찾아 본 결과는 아래와 같고, 두가지 의심가는 원인들 모두가 맞았다.

멀티 DB 사용 시 Transaction을 따로 걸어 주어야하는가?

👉 따로 Transaction을 걸어 줄 필요는 없는 대신, Django 내부에서 지원하는 Transaction 함수의 argument 옵션에 Transaction 대상 DB를 지정해주면된다. 예를들어 A,B라는 서버가 있고 a,b라는 데이터베이스가 있는 상태에서 나는 b 데이터베이스의 데이터를 변경하고자 하여 이 DB에 트랜잭션을 걸어야 할 때, 아래와 같이 명시 해주어서 사용을 하면 된다고한다.

with transaction.atomic(using={target_DB_name}:

using이라는 것을 통해 어떤 DB에 트랜잭션을 걸어 줄 것인지 명시를 해주어야 정확하게 원자성 보장이 동작 하게 되며, 멀티 DB를 활용하는 경우 해당 서버가 바라보는 자신의 DB여도(즉, A 서버가 연결된 a 라는 DB인경우에도) using을 사용해서 해당 DB를 정확하게 명시해주어야 원자성이 보장되는 것이 동작한다고 한다.

공통함수 내에 try-catch 문이 존재하는 경우

👉 해당하는 경우에도 트랜잭션의 원자성이 보장되지 않을 수 있다고 한다.
아래 참고자료에 기술한 블로그에서 알게 된 사실인데, transaction.atomic() 내에서 내부호출로 동작하는 함수 안에 try-catch문이 포함되어 있어, 해당 함수에서 에러가 발생하여 exception이 발생할 경우 transaction.atomic()에서 보장해주는 Rollback이 제대로 동작하지 않을 수 있다고 한다.

그래서 해당 부분에 대해 굳이 사용하고자 한다면, transaction.atomic() 외부를 try-catch로 감싸도록 하는것이 좋다고 한다.

그러면 내부 함수의 try-catch는 어떻게 하는 게 맞는 것인지 살짝 궁금한 점이 생겼는데, 어차피 transaction.atomic() 내에서 try-catch와 같이 예외처리에 대한 rollback을 보장해주기 때문에 굳이 try-catch를 사용하지 않아도 된다는 것을 알았다.
물론 위와 같은 공통적으로 여러곳에서 쓰이는 함수의 경우에는 transaction.atomic()에 쓰이지 않는 곳도 있을 수 있으니 try-catch를 넣는 것도 맞다고 생각하지만, 결국에는 여러곳에서 공통적으로 쓰이는 함수이기때문에 무조건적으로 확실한 테스트가 되어야 공통함수로 빼서 사용할 수 있겠다는 생각도 들었다.

참고자료

profile
코더보다 개발자로, 결과와 과정의 시너지를 만들어 가고 싶은 주니어 개발자

0개의 댓글