Skip to content

트랜잭션 알아보기

Intro

팀원들과 코드리뷰를 하는 중이었다. 팀원 한 분이 내 코드를 (변경에 관련된 코드였다) 보더니 “@Transactional 이 빠졌네요. 이러면 변경 안 될것 같은데요.” 실수로 빼먹은 어노테이션이였지만, 내가 트랜잭션에 대해서 잘 아는지 물음이 생겼다.

트랜잭션 ?

트랜잭션이란 데이터에 대한 하나의 논리적인 작업의 단위를 의미한다. 트랜잭션을 이해하는 쉽고 가장 흔한 예시는 계좌이체다. 송금한 계좌에선 금액이 감소해야 하고 수금한 계좌에선 금액이 증가해야 한다. 송금 계좌와 수금 계좌의 금액 변동이 한 단계에서 이뤄져야 한다. 송금한 계좌에서만 금액이 감소하는 일은 발생하면 안 된다. 이러한 과정들이 모두 성공했을 때, 데이터베이스(DB)에 저장한다(commit). 만약 과정 중에 하나라도 실패한다면, 데이터베이스는 그 과정에서 변경된 어떤 것도 저장하지 않고 처음으로 되돌린다(rollback).

트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질(ACID)

  1. Atomicity(원자성) : 한 마디로 All or Nothing. 설정된 트랜잭션 범위 안의 내용들이 모두 DB에 반영되거나 전혀 반영되지 않아야 한다.
  2. Consistency(일관성) : DB의 상태가 트랜잭션 이전과 이후에 일관성을 유지한다는 의미다. 만약 DB의 제약조건에 위배되는 트랜잭션이라면 트랜잭션은 실행되지 않고 중단된다.
  3. Isolation(독립성) : 여러 트랜잭션이 있을 때, 각각의 트랜잭션은 다른 트랜잭션의 연산에 끼어들지 못하고 독립적이다. A 라는 계좌이체 트랜잭션이 진행중이라면, B 라는 계좌이체 트랜잭션은 동시에 작업할 수 없다.
  4. Durability(영구성) : 트랜잭션이 성공했다면 시스템의 문제 등 어떤 문제가 있더라도, 결과는 반영돼야 한다.

위에 설명한 ACID 성질은 성능 앞에서 타협이 필요하다. 예를 들어 한 데이터에 수십, 수백 개의 연결이 동시에 접근했다. 독립성을 철저히 보장해준다면, 각각의 트랜잭션은 순차적으로 진행된다. 즉, 동시에 접근하는 트랜잭션이 많아질수록 성능은 떨어지게 된다. 이러한 문제를 해결하고자 독립성(Isolation)을 4가지 레벨(Level)로 나눌 수 있다.

4가지 Transaction Isolation Level

  1. READ-UNCOMMITTED : 커밋이 끝나지 않은 데이터를 다른 트랜잭션이 읽을 수 있다. 예를 들어 A,B 순서로 진행중인 트랜잭션이 있다. 트랜잭션 A가 커밋하지 않은 데이터에 트랜잭션 B가 접근해 데이터 값을 읽을 수 있다. 이 경우에 진행중인 트랜잭션 A는 롤백될수도 있기에 데이터 값은 달라질 수 있다. 이렇게 신뢰할 수 없는 데이터를 읽는 것을 DIRTY READ 라고 한다. 즉, 데이터 정합성에 많은 문제가 있기에 실제론 거의 사용하지 않는다.
  2. READ-COMMITTED : 커밋이 끝난 데이터만 다른 트랜잭션이 읽을 수 있다. 커밋을 하지 않은 데이터에는 접근이 불가하다. 예를 들어 A,B 순서로 진행중인 트랜잭션이 있다. 트랜잭션 A가 아직 커밋하지 않았다면, 트랜잭션 B는 트랜잭션 A가 시작되기 전 데이터를 읽어온다. 그리고 A가 커밋이 끝나면 다시 같은 데이터의 값을 읽어온다. 이렇기에 트랜잭션 시작 전, 후 데이터 값이 다를 수 있다. 즉, Non-Repeatable Read 가 발생할 수 있다.
  3. REPEATABLE-READ : 하나의 트랜잭션 내에서 동일 select 쿼리를 실행했을 때, 항상 같은 결과를 가져와야 한다. 이 경우는 데이터가 변경되더라도 같은 데이터를 읽게 해주는 것이다. A 트랜잭션이 select 한 특정 데이터 값이 트랜잭션 B에 의해 값이 바뀌고 B가 커밋까지 되었어도 트랜잭션 A는 처음 select 했던 데이터 값을 가져온다. 이처럼 수정 등의 요청이 발생할 경우에는 원하는 데이터가 안 나올 수 있다. 그렇기에 DB 는 수정 요청에 대해서는 순차적으로 처리하거나 수정할 row 를 미리 잠금으로써 방지한다.
  4. SERIALIZABLE : 하나의 트랜잭션에서 작업 중인 데이터는 다른 트랜잭션에서 접근할 수 없다. 철저한 독립성을 보장하며 동시성을 허용하지 않는다.

트랜잭션 in Spring

Spring 프레임워크에서는 트랜잭션을 주로 @Transactional 어노테이션을 활용한다. 서비스 계층(@Service)에서 트랜잭션을 사용하고 싶은 클래스나 메서드 위에 @Transactional 을 추가해주면 된다. 이 이노테이션이 있으면 메서드가 실행되기 전에 트랜잭션이 시작된다. @Transactional 이 붙은 메서드와 그 메서드가 호출하는 또 다른 메서드까지 모두 한 트랜잭션의 범위다. 그리고 성공적으로 끝나면 커밋을, 실패하면 롤백이 진행된다. 테스트코드(@SpringBootTest) 에서도 @Transactional 어노테이션을 사용한다. 테스트 코드에서는 테스트가 성공해도 테스트 과정에서 진행됐던 쿼리들을 다 롤백시킨다. 이로써 테스트가 DB에 영향을 못 끼치게 한다. Intro에서 말했던 부분은 팀원분 말대로 에러가 발생했다.

javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call

트랜잭션을 사용할 수 있는 EntityManager 가 없다는 에러가 났고, @Transactional 어노테이션을 붙여주니 해결이 됐다. 다음 글에선 EntityManager 에 대해 알아보겠다.


작성자: 김민철

작성일: 2021-12-22