JAVA/JAVA | Spring 학습기록

[Spring] REQUIRES_NEW 옵션만으론 자식이 롤백될 때 부모도 롤백된다

kth990303 2022. 10. 20. 14:24
반응형

두 개 이상의 트랜잭션이 합류할 경우, 트랜잭션 전파레벨 옵션 설정으로 트랜잭션을 관리할 수 있다. 특히, 트랜잭션 전파레벨 중 REQUIRED, REQUIRES_NEW는 실제로도 꽤나 자주 쓰이는 옵션이라 알아두면 더더욱 좋다.

 

전파레벨 옵션에 대해 궁금하다면 아래 글을 살펴보자.

https://kth990303.tistory.com/385

 

[Spring] @Transactional의 전파 레벨에 대해 알아보자

스프링에선 진행중인 트랜잭션에서 다른 트랜잭션이 참여할 때의 합류조건을 설정할 수 있는 Propagation 옵션이 존재한다. DB에서 자체적으로 제공해주는 트랜잭션 격리레벨과 다르게 전파레벨은

kth990303.tistory.com

 

이번 포스팅에서는 REQUIRES_NEW 전파레벨 옵션에 대해 살펴볼 것이므로 REQUIRES_NEW에 대한 정의를 다시 한 번 살펴보자.

 

  • REQUIRES_NEW: Create a new transaction, and suspend the current transaction if one exists.

 

부모 트랜잭션에 REQUIRES_NEW 자식 트랜잭션이 참여하게 되면, 별개로 커넥션을 잡아 자식 트랜잭션을 진행한다. 그렇기 때문에 두 개의 트랜잭션으로 진행된다.

 

이렇기 때문에 아예 독립적으로 아래 요구사항처럼 로직이 진행되는 것으로 오해하는 사람들이 일부 존재한다.

 

  1. 부모 메서드에 예외가 터지면 자식 메서드는 실행되지 않는다.
  2. 자식 메서드에 예외가 터져 트랜잭션이 롤백돼도, 부모 트랜잭션은 커밋된 상태이다. (X)

실제로는 위처럼 독립적으로 로직이 수행되진 않는다.

 

만약 정말 독립적으로 로직이 수행된다면 합류 메서드에 예외가 터져  REQUIRES_NEW 자식 트랜잭션이 롤백될 때, REQUIRED 부모 트랜잭션은 롤백되지 않아야 한다. 하지만 실제 결과는 합류 메서드에 예외가 터져 REQUIRES_NEW 자식 트랜잭션이 롤백되면 REQUIRED 부모 트랜잭션도 롤백이 발생한다. 


학습테스트로 REQUIRES_NEW, 롤백을 알아보자

아래 학습테스트를 실행해보자.

부모 트랜잭션은 gugu를, 자식 트랜잭션에선 kth990303을 저장한다.

부모 트랜잭션은 REQUIRED로, saveSecondTransactionWithRequiresNew() 메서드 자식 트랜잭션은 REQUIRES_NEW 옵션이다.

 

테스트 결과를 살펴보자.

Completing transaction for [transaction.stage2.SecondUserService.saveSecondTransactionWithRequiresNew] after exception: java.lang.RuntimeException (두 번째 트랜잭션 롤백)

 

Completing transaction for [transaction.stage2.FirstUserService.saveFirstTransactionWithRequiredNew] after exception: java.lang.RuntimeException (첫 번째 트랜잭션 롤백)

 

실제로 kth990303, gugu 모두 롤백된 것을 확인할 수 있다.

또한 트랜잭션 2가 진행될 때, 트랜잭션 1이 close된 상태가 아니라는 점도 유의하자.

 

동일 스레드 내에서 별도의 커넥션을 잡아 트랜잭션을 생성하는 것이기 때문이다.

흐름을 아래에 적어보자면 다음과 같다.

 

자식 메서드에서 RuntimeException이 터져 자식 트랜잭션이 롤백 

-> 동일 스레드이므로 부모 메서드도 RuntimeException, 또한 부모트랜잭션 아직 close 안됐음

-> 부모 트랜잭션 또한 롤백

 

그렇다면 아래 요구사항대로 하려면 어떻게 해야 할까?


@TransactionalEventListener

부모 트랜잭션이 진행 완료(커밋 또는 롤백)된 이후에 이벤트를 실행하는 해당 트랜잭션을 진행하도록 하는 어노테이션이다.

Default는 AFTER_COMMIT이다.

 

  • AFTER_COMMIT: Handle the event after the commit has completed successfully.
  • BEFORE_COMMIT: Handle the event before transaction commit.
  • AFTER_ROLLBACK: Handle the event if the transaction has rolled back.
  • AFTER_COMPLETION: Handle the event after the transaction has completed.

 

@TransactionalEventListener를 이용하기 위해 ApplicationEventPublisher를 이용하여 이벤트를 저장시켜놓을 수 있도록 하자.

아래와 같이 유저를 생성하는 Event 클래스를 만들어주자.

 

UserCreateEvent.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class UserCreateEvent {
 
    private final User user;
    private final String account;
 
    public UserCreateEvent(final User user, final String account) {
        this.user = user;
        this.account = account;
    }
 
    public UserCreateEvent(final User user) {
        this.user = user;
        this.account = "hello";
    }
 
    public User getUser() {
        return user;
    }
 
    public String getAccount() {
        return account;
    }
}
 
cs

 

위와 같이 Event 클래스를 생성해줬다면, @TransactionalEventListener 자식 메서드를 부모 메서드에서 실행하지 않아도 된다. 이벤트로 저장만 해두면 된다.

대신, 부모 메서드에서 applicationEventPublisher.publishEvent 메서드를 실행해줘야 하며, 이 때 파라미터로 이벤트를 넘겨주어야 한다. 그렇지 않으면 event parameter is mandatory for event listener method 에러가 발생하게 된다.

 

위처럼 이벤트를 파라미터로 넘겨주고, 자식 메서드를 @TransactionalEventListener로 걸어주면 스프링에서 자체적으로 파라미터로 넘겨진 event를 파악해서 해당 메서드를 부모 트랜잭션 commit 후 실행해준다.

 

따라서 자식 트랜잭션에서 롤백이 일어난다 하더라도, 부모 트랜잭션은 롤백이 되지 않게 된다.

따라서 아래 요구사항을 충족하게 된다.

 

  1. 부모 메서드에 예외가 터지면 자식 메서드는 실행되지 않는다. (부모 트랜잭션이 롤백되면 @TransactionalEventListener의 옵션이 AFTER_COMMIT이기 때문에 자식 트랜잭션은 아예 실행이 되지 않는다.)
  2. 자식 메서드에 예외가 터져 트랜잭션이 롤백돼도, 부모 트랜잭션은 커밋된 상태여야 한다. (부모 트랜잭션은 이미 커밋됐기 때문이다.)

 

주의할 점은, 위의 메서드처럼 @TransactionalEventListener로 실행할 트랜잭션에서 insert, update, delete 연산을 수행할 경우 전파레벨을 REQUIRES_NEW로 걸어주어야 한다.

@Transactional의 디폴트 옵션인 REQUIRED는 자식 트랜잭션이 부모 트랜잭션에 합류해서 하나의 트랜잭션으로 작동된다. 그렇기 때문에 트랜잭션 커밋 후 이벤트에서 해당 연산이 수행된다 해도 의도대로 동작되지 않는다. 동일한 트랜잭션에서 이미 커밋을 한 후에 연산을 수행한 것이기 때문이다. 따라서 별도의 트랜잭션으로 관리되도록 REQUIRES_NEW로 진행해준다.

 

위와 같이 자식을 REQUIRES_NEW, @TransactionalEventListener로 등록해주면 아래 학습테스트가 통과한다.


이후에 내편 프로젝트에 해당 기술을 적용한 경험을 작성해볼 예정이다.

 

참고

반응형