Transaction은 무엇이고 왜 이용할까?

SeokHwan An·2024년 2월 26일
1

DB

목록 보기
3/4

백엔드 개발자는 비즈니스 로직을 개발하면서 변화된 데이터를 DB에 반영해주는 작업을 진행합니다. 간단한 CRUD의 경우에는 SQL 하나가 작업 단위인 경우가 있지만 복잡한 비즈니스 사항을 구현하다보면 한번에 여러 데이터가 변화하는 상황이 많습니다. 이 때 데이터 변화가 한번에 동작하지 않으면 어떤 문제를 발생할까요? 간단하게 송금을 해주는 예시를 살펴보겠습니다.

Member.java

MemberService.java

위의 코드는 Member A가 targetMember B에게 돈을 송금해주는 비즈니스 로직입니다. 이 때 우리와 같은 백엔드 개발자는 A의 계좌에서 돈이 출금되는 것, 사용자 B의 계좌에 돈이 입금되는 것이 한번에 이루어지기 기대할 것입니다. 이를 테스트 코드를 통해서 살펴보면 다음과 같습니다.

  1. MemberA와 MemberB를 DB에 저장
  2. MemberA가 MemberB에게 5000원을 송금
  3. 송금 후 MemberA와 MemberB의 잔액확인

현재는 중간에 서버가 다운된다거나 실패하는 경우가 발생하지 않아서 큰 문제가 발생하지 않고 테스트가 통과하는 것을 볼 수 있습니다.

만약 위의 작업(MemberA에서 돈을 출금하는 것, MemberB에 돈을 입금하는 것)이 동시에 이루어지지 않는 경우에는 어떤 문제가 발생할 수 있을까요? 사용자 A에서는 5000원이 출금되었는데 사용자 B에는 5000원이 입금되지 않는 상황이 발생할 수 있습니다. 이와 같은 상황을 테스트로 구현하여 확인해보겠습니다.

데이터의 일관성이 맞지 않는 문제점 발생

제가 의도한 부분은 memberA의 경우는 출금이 되었는데 memberB에 입금이 되지 않는 상황입니다.

변화된 MemberService.java

기존의 코드에서 MemberB가 돈을 받기 전에 예외를 발생시켜 코드가 올바르게 동작하지 않게 구성했습니다. 이게 실제 상황이라고 가정하면 우리는 송금하는 도중에 문제가 발생한 것이기 때문에 MemberA도 금액이 출금되지 않은 것이라 기대를 하겠지만 실제는 그렇지 않습니다.

위와 같이 테스트 코드를 통해 송금 실패 후 MemberA와 MemberB의 잔액을 확인해보면 다음과 같습니다.

MemberA의 금액은 줄었지만 MemberB의 금액은 늘어나지 않았습니다. 이와 같은 일이 실생활에서 발생한다면 큰 문제로 이어져 결국 서비스가 존속하지 못하는 원인이 될 것입니다. 그러면 우리는 어떻게 하나 이상에 데이터를 변경하는 작업을 마치 하나의 작업처럼 처리할 수 있을까요? 트랜잭션이 이 문제에 해결책이 될 수 있습니다.

트랜잭션이란

Oracle에서 정의한 트랜잭션의 의미는 다음과 같습니다.

transaction is a logical, atomic unit of work that contains one or more SQL statements.

A transaction groups SQL statements so that they are either all committed, which means they are applied to the database, or all rolled back, which means they are undone from the database.

트랜잭션은 하나 혹은 그 이상의 SQL문을 한번에 처리하는 논리적인 작업 단위라고 합니다. 즉, 여러개의 SQL작업이 한번에 db에 적용되거나 혹은 모두 적용되지 않는다고 합니다.

트랜잭션의 동작방식은 트랜잭션이 시작을 하고 내부적으로 작업들(insert, update, delete 등)이 발생한 후에 이를 db에 반영할 것이면 커밋을 그렇지 않으면 롤백을 하는 방식으로 동작을 합니다.

우리가 앞서서 보았던 예시의 경우 MemberA에서 돈이 출금되는 것, MemberB에 돈이 입금되는 것은 각각 SQL작업이었습니다. 하지만 송금이라는 관점에서는 이 두 작업은 마치 하나처럼 동작해야 합니다. 그래서 이 부분에는 하나의 트랜잭션으로 묶어서 처리해야 합니다.

💡 트랜잭션은 특정 작업에만 이용되는 것인가요?
아닙니다. 트랜잭션은 모든 SQL에서 작동을 하는데 그 이유는 기본적으로 db의 오토커밋이 설정되어 있기 때문에 바로바로 db에 반영됩니다. 
여러 SQL 작업을 하나의 트랜잭션에 묶어서 처리하기 위해서는 오토커밋 설정을 끈 후 작업을 하고 마지막에 커밋이나 반영하기 싫은 경우 롤백을 하면됩니다.

스프링에서 트랜잭션 이용하기

스프링에서 트랜잭션은 @transactional 를 통해 이용할 수 있습니다. @transactional 이 동작하는 것을 정확하게 알기 위해서는 AOP 개념과 Proxy에 대해서 알아야 하지만 이 글에서는 다루지는 않겠습니다. 예시에서 사용된 withdraw 메소드에 @transactional 을 붙여주게 되면 실제로는 코드가 다음과 같이 동작을 하게 됩니다.

  1. 오토커밋 설정을 끈다.
  2. 개발자가 작성한 코드가 동작을 한다.(기존의 withdraw 코드 동작)
  3. 문제가 발생하지 않는 경우 커밋이 실행되고 문제가 발생하면 롤백이 실행된다.

위의 과정을 통해서 아까와 같이 비즈니스 로직이 실행되는 도중에 예외나 문제가 발생하면 일부 변화만 db에 반영되는 것이 아닌 진행했던 모든 DB 작업들이 롤백 처리가 되어서 데이터의 정합성의 문제가 발생하지 않을 것입니다. @Transactional 를 붙힌 후 다시 송금을 실패하는 테스트 코드의 결과를 보겠습니다.

결과를 보면 알 수 있듯이 트랜잭션으로 묶인 작업에서 예외가 중간에 발생하는 경우에는 이전의 작업 모두 롤백이 되는 것을 확인할 수 있습니다. (우리가 원하는 방식인 송금이 실패했을 때에는 MemberA의 잔액도 변화하지 않는 것을 확인할 수 있습니다.)

다음 글에서는 트랜잭션의 ACID와 트랜잭션의 격리 수준에 대해서 다루어보겠습니다.

0개의 댓글