Spring Boot, Which one should i use @Transactional ?

Which one should you use: @javax.transaction.Transactional or @org.springframework.transaction.Transactional?
김주혁's avatar
Aug 20, 2024
Spring Boot, Which one should i use @Transactional ?
 
 

@Transactional

 
Spring Transactional annotation은 2 종류가 있다.
  • @javax.transaction.Transactional
  • @org.springframework.transaction.Transactional
 

@javax.transaction.Transactional(Java EE @Transactional)

 
이 어노테이션은 Spring JAVA EE 7에 추가 됐다. JAVA EE 어노테이션은 3개의 속성만을 정의한다.
  • value :
    • Enum 형태(TxType)로 제공되는 propagation(전파) 전략이다.
      • REQUIRED :
        • 현재 트랜잭션이 존재하면 그 트랜잭션을 사용하고, 존재하지 않으면 새로운 트랜잭션을 생성한다.
        • @Transactional(propagation = Propagation.REQUIRED) public void transaction() { // Current Transaction: CommonService.transaction System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); transactUtils.testTransact(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void testTransact() { // Current Transaction: CommonService.transaction System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); } // 트랜잭션 이름이 같다. 부모 트랜잭션을 이어 받음
      • REQUIRES_NEW :
        • 항상 새로운 트랜잭션을 생성한다. 현재 트랜잭션이 존재하면 일시 중지된다.
        • 특정 작업이 독립적으로 수행되어야 할 때 사용한다.
        • 예를 들어, 로그 기록이나 알림 전송과 같은 작업에서 유용하다.
          • 부모 트랜잭션이 살아 있고, REQUIRES_NEW가 선언된 자식 트랜잭션이 동작할 때 자식 트랜잭션에서 Exception을 발생시키면 트랜잭션 동작은 어떻게 돌아가게 될까?
            • 정답은 부모 트랜잭션까지 같이 롤백된다. 자식 트랜잭션에서 발생한 Exceptiopn이 부모 트랜잭션에게 까지 전파되기 때문이다. 자바에서는 예외가 발생하면 중간에 별도 처리를 하지 않는 이상 콜 스택을 따라 처음 호출한 곳 까지 Exception이 전파된다.
            @Transactional(propagation = Propagation.REQUIRED) public void transaction() { // CommonService.transaction System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); transactUtils.testTransact(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void testTransact() { // TransactUtils.testTransact System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); } // 트랜잭션 이름이 다르다. REQUIRES_NEW는 새로운 트랜잭션
      • MANDATORY :
        • 현재 스코프에 트랜잭션이 반드시 존재해야 한다. 존재하지 않으면 예외가 발생한다.
        • 트랜잭션이 항상 필요할 때 사용한다.
        • 데이터 일관성을 보장해야 하는 경우에 적합하다.
          • @Transactional( propagation = Propagation.MANDATORY ) public void postUser(PostUserDto dto) // org.springframework.transaction.IllegalTransactionStateException: // No existing transaction found for transaction marked with // propagation 'mandatory'
      • SUPPORTS :
        • 현재 트랜잭션이 존재하면 그 트랜잭션을 사용하고, 존재하지 않으면 트랜잭션 없이 실행된다.
        • Read 작업에서 사용될 수 있다.
      • NOT_SUPPORTED :
        • 항상 트랜잭션 없이 실행된다. 현재 트랜잭션이 존재하면 일시 중지된다.
          • 실제로는 새로운 트랜잭션을 만들어 별도고 관리하는 걸로 판단된다. REQUIRES_NEW와 보이는 것만 비교한다면 똑같이 동작한다.
            • @Transactional(propagation = Propagation.REQUIRED) public void transaction() { // Current Transaction: CommonService.transaction System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); transactUtils.testTransact(); } @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testTransact() { // Current Transaction: TransactUtils.testTransact System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); } ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ public void transaction() { // null System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); transactUtils.testTransact(); } @Transactional(propagation = Propagation.NOT_SUPPORTED) public void testTransact() { // Current Transaction: TransactUtils.testTransact System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); }
        • 트랜잭션이 있는 상태에서 실행하면 안 되는 작업에 사용한다.
      • NEVER :
        • 트랜잭션이 존재해서는 안된다. 존재하면 예외가 발생한다.
        • Spring Bean의 메서드가 트랜잭션이 없는 상태에서 호출되면 정상 실행된다.
        • 트랜잭션이 활성화된 상태에서 호출되면, InvalidTransactionException이 포함된 TransactionalException이 발생한다.
          • @Transactional(propagation = Propagation.REQUIRED) public void transaction() { System.out.println("Transaction_1"); transactUtils.testTransact(); } @Transactional(propagation = Propagation.NEVER) public void testTransact() { System.out.println("TEST Transact On"); } // org.springframework.transaction.IllegalTransactionStateException: // Existing transaction found for transaction marked with propagation // 'never'
      • NESTED :
        • JAVA EE에는 없는 value type이다.
 
  • dontRollbackOn :
    • 이 요소는 특정 예외가 발생했을 때 트랜잭션을 롤백하지 않도록 지정한다. 특정 비즈니스 로직에서 발생할 수 있는 예외가 발생했을 때 트랜잭션을 이어가고 싶을 때 사용한다.
    • 기본 값으로 빈 배열 {}이 설정되있다.
    • 특정 CustomException이 발생했을 때 유지하는 케이스로 사용된다.
    • rollbackOn과 dontRollbackOn이 동시에 지정된 경우, dontRollbackOn이 우선 적용된다.
 
  • rollbackOn :
    • 특정 예외가 발생했을 때 트랜잭션을 롤백해야 한다고 지정한다. 즉, 지정한 Exception이 발생하면 진행 중인 트랜잭션이 취소되고, 모든 변경사항이 원래대로 돌아간다.
    • 데이터베이스 작업 중 치명적인 오류(DataAccessException)에 대해 트랜잭션을 롤백할 때 사용한다.
    • 기본적으로는 빈 배열 {}이 설정되있고, 특별히 어떤 값을 지정하지 않으면 모든 예외에 대해 롤백이 발생한다.
 

@org.springframework.transaction.Transactional(Spring @Transactional)

 
이 어노테이션은 Spring framework 1.2에 추가 됐다.
 
  • isolation :
    • 기본 데이터베이스 격리 수준을 정할 수 있다.
    • READ_UNCOMMITTED :
      • 다른 트랜잭션의 커밋되지 않은 변경 사항을 읽을 수 있다.
      • 가장 낮은 격리 수준으로, 더티 리드(Dirty Read)가 발생할 수 있다.
    • READ_COMMITTED:
      • 다른 트랜잭션이 커밋한 변경 사항만 읽을 수 있다.
      • 더티 리드는 방지되지만, 논리적 일관성이 보장되지 않을 수 있다.
    • REPEATABLE_READ:
      • 같은 트랜잭션 내에서 동일한 쿼리를 여러 번 실행할 때 항상 같은 결과를 보장한다.
      • 팬텀 리드(Phantom Read)가 발생할 수 있다.
    • SERIALIZABLE:
      • 가장 높은 격리 수준으로, 트랜잭션이 완전히 독립적으로 실행된다.
      • 모든 트랜잭션이 순차적으로 실행되는 것처럼 동작한다.
  • Propagation :
    • Enum 형태(Propagation)로 제공되는 propagation(전파) 전략이다.
      • NESTED를 제외하고 JAVE EE의 value와(TxType) 같은 값 및 동작을 한다.
      • REQUIRED
      • REQUIRES_NEW
      • MANDATORY
      • SUPPORTS
      • NOT_SUPPORTED
      • NESTED :
        • 현재 트랜잭션이 존재하면 중첩 트랜잭션을 생성한다.
        • 존재하지 않으면 새로운 트랜잭션을 생성한다.
        • 중첩 트랜잭션은 부모 트랜잭션이 롤백되면 함께 롤백된다.
        • 복잡한 트랜잭션 구조에서 부분적으로 롤백할 수 있는 기능이 필요할 때 사용한다.
          • @Transactional(propagation = Propagation.REQUIRED) public void transaction() { // Current Transaction: CommonService.transaction System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); transactUtils.testTransact(); } @Transactional(propagation = Propagation.NESTED) public void testTransact() { // Current Transaction: CommonService.transaction System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); } // 트랜잭션 이름이 같다. 부모 트랜잭션을 이어 받음 ㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡㅡ public void transaction() { // null System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); transactUtils.testTransact(); } @Transactional(propagation = Propagation.NESTED) public void testTransact() { // Current Transaction: TransactUtils.testTransact System.out.println("Current Transaction: " + TransactionSynchronizationManager.getCurrentTransactionName()); } // 자식 트랜잭션을 생성해서 사용
  • value / TransactionManager :
    • transactionManager 속성은 @Transactional 애너테이션이 적용된 메서드 또는 클래스에서 사용할 트랜잭션 매니저를 명시적으로 지정한다.
    • 기본적으로 Spring은 PlatformTransactionManager 인터페이스를 구현한 트랜잭션 매니저를 자동으로 선택하지만, 여러 데이터 소스나 트랜잭션 매니저가 있는 경우 이 속성을 사용하여 특정 매니저를 지정할 수 있다.
    • 하나의 프로젝트가 여러 데이터 소스에 연결되 있을 때 사용할 수 있다.
      • 하나의 Application에 MariaDb, MySQL, MongoDB 연결 등
      • 하나의 Application에 MyBatis, JPA, JDBC를 같이 쓰는 경우 등 …
  • Timeout :
    • 트랜잭션의 타임아웃을 지정하는 선택적 속성이다. 이 속성은 REQUIRED / REQUIRES_NEW 전파 값에만 적용된다. 시간 내에 트랜잭션이 완료되지 않으면 자동으로 롤백된다.
  • TimeoutString :
    • 타임아웃을 문자열로 지정한다. ${timeout} 과 같은 플레이스 홀더를 사용하여 동적으로 타임아웃 값을 지정할 수 있다.
  • readOnly :
    • 트랜잭션이 읽기 전용인지 읽기-쓰기인지 지정하는 속성이다. 이 속성은 REQUIRED 또는 REQUIRES_NEW 전파 값에만 적용된다. 읽기 전용 트랜잭션은 성능 최적화를 위해 데이터베이스에 대한 쓰기 작업을 방지한다.
    • readOnly 는 변경감지 , flush 등을 사용하지 않아 의도치않은 데이터변경을 막아준다.
    • 영속성 컨텍스트는 Entity 조회시에 초기 상태(변경 전 Entity, 처음 조회될 때)를 Snapshot에 저장하게 된다. 트랜잭션이 Commint 될 때, 초기 값인 Snapshot과 커밋된 내역으로 변경된 Entity를 비교하여 변경된 내용에 대해 update나 delete query를 생성 해 쓰기 지연 저장소에 저장한다. 그 후 일괄적으로 쓰기 지연 저장소에 있는 query들을 flush하고 트랜잭션을 Commit함으로 update 없이도 JPA에서 save 만으로도 Entity의 수정이 이루어지는데, 이를 변경 감지(Dirty Checking)이라고 한다. 이 때 readOnly를 true로 설정하면 JPA는 트랜잭션 내에서 조회하는 Entity가 조회용임을 인식하고 변경 감지를 위한 Snapshot을 따로 보관하지 않기 때문에 메모리 상 이점이 있다. (참조 cc : )
  • rollbackFor :
    • 이 배열안에 들어간 Exception이 발생하면 트랜잭션이 롤백된다.
    • [ Exception.class, RuntimeException.class, CustomException.class .. ]
  • rollbackForClassName
    • 이 배열 안에 들어간 이름 패턴과 일치하는 예외가 발생하면 트랜잭션이 롤백된다.
    • [ com.example.MyException, com.example.BadRequestExcpetion … ]
  • noRollbackFor
    • 이 배열안에 들어간 Exception은 발생해도 트랜잭션 롤백을 발생시키지 않는다.
  • noRollbackForClassName
    • 이 배열안에 들어간 예외 이름 패턴은 예외가 발생해도 트랜잭션 롤백되지 않는다.
    • @Transactional( rollbackFor = DuplicateUserException.class, noRollbackFor = IllegalArgumentException.class )
  • label
    • label은 트랜잭션에 대한 설명을 추가하는 문자열 배열이다. 트랜잭션의 의미나 목적을 명확히 하고, 트랜잭션 매니저가 이를 기반으로 특정 동작을 수행하도록한다.
    • 사용 목적
      • 트랜잭션 식별:여러 트랜잭션이 존재하는 시스템에서 각 트랜잭션을 구분하기 위해 레이블을 사용할 수 있다. 예를 들어, "UserCreation", "OrderProcessing"과 같은 레이블을 통해 어떤 트랜잭션이 어떤 작업을 수행하는지 쉽게 식별할 수 있다.
      • 로깅 및 모니터링:트랜잭션 매니저는 레이블을 사용하여 트랜잭션의 상태를 로깅하거나 모니터링할 수 있다. 특정 레이블이 붙은 트랜잭션이 실패했을 때, 해당 레이블을 기반으로 문제를 추적하고 분석할 수 있다.
      • 정책 적용:특정 레이블에 따라 트랜잭션에 대한 정책을 다르게 적용할 수 있다. 예를 들어, "Critical"이라는 레이블이 붙은 트랜잭션은 더 높은 격리 수준이나 롤백 정책을 적용받을 수 있다.
      • 비즈니스 로직과의 연계:비즈니스 로직에서 특정 레이블을 기반으로 조건부 처리를 할 수 있다. 특정 레이블이 있는 트랜잭션에 대해서만 추가적인 검증이나 후처리를 수행할 수 있다.
    • 예시
      • @Transactional(label = {"UserCreation", "Critical"}) public void createUser(User user) { // 사용자 생성 로직 }
 

어떤 어노테이션을 사용해야 할까?

 
정답은 Spring annotation이다. 그 이유로는
 
  • 쉽게 격리 수준에 대한 동작을 테스트할 수 있다.
    • JAVA EE 7의 경우 아래 Spring Proxy Objects의 사례를 검증할 수 없다. 이것 외에도 몇 가지 테스트가 내가 예측한대로 동작하지 않았다.
  • 더 많은 기능을 지원한다.
  • Java EE만 사용하고 Java EE 애플리케이션 서버에 애플리케이션을 배포하는 경우 Java EE @Transactional어노테이션을 사용하면 된다.
 

Spring Proxy Objects

 
@Transactional 어노테이션은 Spring의 프록시 객체에만 적용된다. 즉, Spring이 관리하는 빈(Bean)에서 메서드를 호출할 때 그 메서드에 설정된 트랜잭션 속성이 적용되는 것이다.
 
예를 들어, UserService라는 객체가 있고 이 서비스에 signUp이라는 메서드가 있다고 가정해본다면
트랜잭션이 다음과 같이 설정되어 있다고 할 때:
java// Service 객체 안에 메서드 둘 다 존재 @Transactional public User signUp(SignUpRequest req) { // true System.out.println("Current Transaction: " + TransactionSynchronizationManager.isActualTransactionActive()); ... getUserByEmail() } @Transactional(propagation = Propagation.NEVER) public User getUserByEmail(String email) { // true System.out.println("Current Transaction: " + TransactionSynchronizationManager.isActualTransactionActive()); ... return user; }
 
여기서 getUserByEmail 메서드는 Propagation.NEVER로 설정되어 있으므로, 트랜잭션이 존재하지 않을 때 호출되면 에러가 발생해야 한다. 하지만 실제로는 에러가 발생하지 않는다. 그 이유는 왜일까?
 
이유는 Spring Proxy를 통해 호출되어야만 트랜잭션 속성이 적용되기 때문이다. getUserByEmail은 같은 클래스 내에서 직접 호출되었기 때문에 Spring Proxy를 우회하여 단순히 같은 객체 내에서 호출된 것에 불과하다.
 
그렇다면 Propagation.NEVER의 조건을 만족시켜 IllegalTransactionStateException을 발생시키려면 어떻게 해야 할까?
 
정답은 바로, Spring Proxy에 의해 호출되도록 같은 객체 내에서 직접 호출하지 않도록 수정해야 한다. 따라서 별도로 트랜잭션을 관리하고 싶은 기능은 다른 Spring Bean으로 나누어 UserService에서 호출하도록 수정해야 하는데 그 예시는 아래와 같다.
 
@Service @RequiredArgsConstructor public class UserService { private final UserReader userReader; @Transactional public User signUp(SignUpRequest req) { // true System.out.println("Current Transaction: " + TransactionSynchronizationManager.isActualTransactionActive()); ... userReader.getUserByEmail() } } @Component public class UserReader { @Transactional(propagation = Propagation.NEVER) public User getUserByEmail(String email) { // true System.out.println("Current Transaction: " + TransactionSynchronizationManager.isActualTransactionActive()); ... return user; } }
 
변경된 차이점은 이렇다 :
userReader.getUserByEmail() 처럼 주입된 다른 객체를 통해 호출함으로써 Spring Proxy를 통해 호출되도록 수정되었다.
 
이렇게 수정한 후 기능을 호출하면 다음과 같은 에러가 발생한다
 
javaResolved [org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never']
 
이처럼 Spring의 트랜잭션 관리에서 프록시의 역할과 메서드 호출 방식이 얼마나 중요한지를 알 수 있다.
 
 
 
 
Share article
RSSPowered by inblog