[Spring Data] @Transactional 어노테이션으로 보는 Spring의 트랜잭션 이야기

반응형

Spring을 한 번 쯤 사용해보셨다면 Transactional 어노테이션에 대해 알고계실 것입니다. 그런데 무작정 썼을 땐 DB랑 연결하고 객체 데이터를 영속할 때 이거 쓰면 돼. 라고 생각하시는 분들이 계셨을 것입니다.

 

하지만 실제로 이 어노테이션을 사용했을 때와 그렇지 않았을 때 왜 이런 차이가 발생하는지에 대해 한 번 쯤 궁금증을 가져보신 분들이 계신다면 이 글을 차분히 읽어 보셨으면 좋겠습니다.

 

 

 

Transaction

트랜잭션에 대해서는 다른 글에서도 충분히 언급되어 있는 내용이지만 여기서 한 번 더 설명을 드리면, 데이터베이스의 트랜잭션을 이야기하며 DBMS 혹은 그 유사한 시스템에서 발생하는 연산들의 상호작용 단위입니다. 

 

좀 더 쉽게 설명을 해보면, 우리는 데이터 하나를 영속하거나 변경하기 위해 여러 작업들을 수행하는데, 이들은 성공할 수도 있고, 실패할 수도 있습니다. 예를 들어, 내 계좌에 있는 40,000원 중 Rye라는 사람에게 30,000원을 송금하려고 합니다. 그런데, 그러는 도중 네트워크 장애로 인하여 내 계좌의 잔액은 10,000원이 되었지만 Rye라는 사람은 30,000원을 받지 못하였습니다.

 

이런 경우는 어떻게 문제를 해결할 수 있을까요?

 

우리는 송금이라는 일련의 과정을 아래의 순서로 처리하게 됩니다.

 

  1. 내 계좌에 있는 40,000원에서 30,000원을 차감한다.
  2. Rye 계좌에 30,000을 추가한다.
  3. 송금을 완료한다.

우리는 이 일련의 과정을 하나의 트랜잭션이라고 이야기하는데, 여기서 만약 중간에 오류가 발생하면 이 트랜잭션은 실패로 끝나게 되어 내 계좌에 있던 변동 사항은 ROLLBACK되어 집니다.

 

위 사항을 더 간략하게 정리하면 아래와 같습니다.

 

트랜잭션 내 연산은 모두 독립적으로 이루어지며, 그 과정 중간에 다른 연산을 수행할 수 없고, 하나의 연산이라도 오류가 생겼다면 해당 트랜잭션은 취소되고 모두 원래대로 되돌아가야 한다. 반드시 모든 연산이 성공해야만 트랜잭션이 성공됐다고 본다.

 

이러한 트랜잭션의 원칙은 의도치 않은 값이 저장되거나 조회되는 걸 막을 수 있습니다. 이러한 원칙을 잘 정리한 것을 바로 ACID라고 합니다.

 

https://ko.wikipedia.org/wiki/ACID

 

ACID - 위키백과, 우리 모두의 백과사전

다른 뜻에 대해서는 애시드 문서를 참고하십시오. ACID(원자성, 일관성, 고립성, 지속성)는 데이터베이스 트랜잭션이 안전하게 수행된다는 것을 보장하기 위한 성질을 가리키는 약어이다. 짐 그

ko.wikipedia.org

 

 

 

@Transactional

Transactional 어노테이션은 Spring Framework에서 제공하는 어노테이션입니다. 이 어노테이션은 수행하는 작업에 대해 위 트랜잭션 원칙이 지켜지도록 보장해주는 것으로 직접 객체를 만들지 않고, 선언만 해도 이 과정이 적용되어서 선언적 트랜잭션이라고도 표현합니다.

 

이번엔 온라인 커피숍에서 커피를 주문하는 시나리오를 예시로 보겠습니다. 코드를 보면 하나는 주문 내역을 가져오는 메서드이고, 하나는 주문을 생성 메서드입니다. 주문을 생성하는 메서드의 경우 주문 내역 생성 후 결제를 진행하게 되는데, 만약 결제 진행 메서드에서 결제가 실패하면 생성된 주문 내역은 Rollback 되어 없던 주문으로 인식됩니다.

 

하지만 여기에는 모순이 있습니다.

 

@Transactional은 기본적으로 UnChecked Exception, Error만을 롤백한다.

 

여기서 UnChecked Exception이란, 에기치 못한 오류를 말합니다. 예기치 못한 오류란, 개발자가 정의한 오류가 아닌 프레임워크나 애플리케이션에 의해 발생되는 오류를 말하는데, 만약 사용자가 체크 카드로 결제했을 때 잔액이 부족해서 생긴 결제 실패라던지, 카드 정지, 카드 유효기간 이슈 등으로 발생한 정의된 오류에 대해서는 롤백하지 않는다는 것입니다.

 

따라서 이런 경우는 Transactional 어노테이션에서 rollbackedFor 파라미터로 롤백할 Exception 클래스를 정의하면 됩니다.

 

이번엔 주문 내역을 가져오는 코드를 보겠습니다. 단순히 코드를 봤을 때는 Transactional이 필요없을 정도인데, 왜 여기서 Transactional이 필요할까요?

 

  • 데이터를 가져오는 도중, 다른 메서드나 함수에서 해당 값을 접근했을 때 잘못된 값을 가져오지 않도록 함.
  • 만약 해당 메서드에서 수정하려는 데이터를 접근하여 다른 메서드나 함수에서 데이터를 수정하는 행위를 했을 때 오류가 발생했을 경우, 해당 변경 사항이 반영(commit)되지 않도록 함.

 

이 부분은 ACID 중 원자성에 해당합니다. 어떤 작업이 완전하게 성공, 혹은 실패로만 되어야 하며 중간에 성공한 결과가 있더라도 최종 부분까지 왔을 때 성공하지 못했다면 그 작업은 실패로 끝나게 되는 것입니다. 

 

 

 

@Transactional의 작동 원리

그러면 도대체 이 Transactional은 어떤식으로 동작하는 걸까요? 아마 JPA를 공부해보신 분들이라면 이 어노테이션이 마냥 JPA에서만 동작하는 어노테이션일 것이라고 생각할 수 있을 것입니다.

 

하지만 본래 Transactional 어노테이션은 Spring Framework에서 지원하는 AOP 방식의 어노테이션이며 JPA 뿐만 아니라 Spring Data로 구현된 R2DBC, JDBC에서도 모두 적용되는 어노테이션입니다.

(더 중요한 것은 JPA도 JDBC를 사용한다는 것입니다)

 

AOP에 대해서 자세히 알아보시려면, 아래의 글을 참고해보세요.

 

 

[Spring] AOP (Aspect-Oriented Programming) 기본과 개념

Spring에는 AOP(Aspect-Oriented Programming, 관점 지향 프로그래밍)이라는 핵심 기능을 제공합니다. 음? OOP(객체 지향 프로그래밍)라는 것은 들어봤는데, AOP는 무엇일까요? AOP vs OOP ? 관점 지향 프로그래밍

blog.neonkid.xyz

우리는 Transactional의 원리를 이해하기 위해 실제로 Java에서 데이터를 데이터베이스에 어떻게 영속하는지에 대해 알아볼 것입니다.

 

위 코드는 실제 Java에서 JDBC를 이용해 DataSource(DB에 연결하기 위한 데이터베이스 연결 정보를 담는 인터페이스)와 이를 통해 연결 객체인 Connection을 이용해서 DB에 트랜잭션을 수행하는 모습입니다.

 

실제로 Transactional 어노테이션은 위 두 코드를 기반으로 움직이며 Transactional에서 기본 Exception은 위에서 말한것처럼 UnChecked Exception이 기본값인 형태로 동작합니다. 그리고 이러한 코드를 내가 만든 메서드에 삽입하게 되는데, JVM에서 이렇게 하는 방법에는 바이트 코드를 삽입하는 방법과 프록시 객체를 이용하는 방법이 있고, 스프링에서는 후자를 사용합니다. 이 부분은 차후 다른 글에서 깊게 다뤄 볼 것입니다.

 

간단하게는 Transactional이 위와 같이 동작하고, JPA에서는 JPA가 가지고 있는 영속성 컨텍스트를 통해 동작합니다.

 

JPA의 영속성 컨텍스트를 통해 Entity 객체를 영속하는 역할을 담당하고 그들의 트랜잭션은 Transactional 어노테이션이 진행하는 걸로 보면 됩니다. 이러한 방식으로 영속성 컨텍스트가 독립적으로 움직이기 때문에 @Transactional을 써도 본래 트랜잭션의 원칙을 정확히 지킬 수 있습니다.

 

그렇다면 Transactional 어노테이션이 선언된 메서드에서 별도의 EntityManager 객체를 쓰는 경우는 어떻게 되나요?

 

Spring에서는 같은 트랜잭션 내에서 여러개의 EntityManager 인스턴스를 생성해도 하나의 EntityManager로 동작합니다. 따라서 위의 경우 영속성 컨텍스트 1개만을 사용합니다. 이 이야기는 차후 propagation을 다루면서 자세히 이야기 해보도록 하겠습니다.

 

 

 

@Transactional 옵션

마지막으로 Transactional 어노테이션의 옵션에 대해 다뤄보도록 하겠습니다. 

 

  • isolation

    트랜잭션에서 일관성 없는 데이터 허용 수준을 설정하는 값으로 JDBC의 isolation으로 동작합니다.

  • propagation

    트랜잭션 수행 중 다른 트랜잭션 수행에 끼치는 영향을 설정하는 값입니다.

  • noRollbackFor

    특정 예외가 발생하는 클래스에 대해서는 rollback을 수행하지 않는 옵션입니다.

  • rollbackFor

    특정 예외가 발생하는 클래스에 대해 rollback을 수행하는 옵션입니다. (기본적으로 UnChecked를 포함합니다.)

  • timeout

    지정한 시간 내 메서드 수행이 완료되지 않는 경우, rollback 하는 옵션입니다. (-1인 경우 무제한, 기본값)

  • readOnly

    트랜잭션을 읽기 전용으로 사용합니다. (DML 연산이 수행되는 경우 예외 발생)

 

 

 

마치며...

최근에 Spring Data JDBC를 다루고 나서 Spring에 있는 Transactional 어노테이션이 단순히 JPA만을 위한 게 아니라는 것을 알았고, 작성하고 난 뒤에서야 Spring은 역시 Framework 라는 걸  다시 한 번 뼈저리게 느껴졌습니다.

 

본래 우리가 Transactional 없이 애플리케이션 서버를 만들고 데이터베이스에 영속하려면 커넥션을 만들고, 영속하고자 하는 데이터를 클래스화 하거나, Set으로 묶어 데이터베이스 서버로 보내야 합니다. 그 뿐만 아니라 트랜잭션이 발생하는 일련의 과정 (begin ~ commit)을 직접 코드에 담아서 구현해야 비로소 DB 서버와 연결된 애플리케이션 서버가 완성됩니다.

 

이 외에도 Spring에서 Transactional 어노테이션을 사용하기 위해서는 Spring Configuration에 @EnableTransactionManagement 어노테이션을 이용하여 TransactionManager 인터페이스에 DB 연결 정보 객체인 DataSource를 주입하는 등의 작업을 거쳐야 하지만 Spring boot에서 이러한 설정들을 모두 템플릿처럼 잘 만들어놨기 때문에 우리가 Spring boot를 쓸 때 만큼은 최대한 애플리케이션 개발에 집중할 수 있다는 것을 알아두셔야 할 것입니다.

 

 

 
반응형

Tistory Comments 0