공부하는 블로그

Spring | @Transcational - propagation 본문

카테고리 없음

Spring | @Transcational - propagation

치킨닮은닭 2023. 10. 1. 23:02

@Transactional?

데이터 베이스에 트랜잭션 범위를 알린다. @Transactional이 선언된 메서드가 수행될 때, DB는 auto commit을 일시적으로 껐다가 메서드가 종료되는 시점에 메서드 결과에 따라 commit 또는 rollback을 수행하고 auto commit 옵션을 다시 켠다.

Propagation

@Transactional 어노테이션에 옵션 중 하나로 트랜잭션의 전파 레벨을 설정할 수 있다. 전파 레벨은 하나의 트랜잭션이 수행 중일 때, 같은 TransactionManager 내에서 추가 트랜잭션이 발생하게 된다면 어떤 방식으로 새로운 트랜잭션을 진행할 지 결정하는 것이다. 스프링에서는 7가지의 전파 레벨을 지원한다.

  1. REQUIRED
    • 기존 트랜잭션이 있으면 그 트랜잭션을 사용한다.
  2. SUPPORTS
    • 기존 트랜잭션이 있으면 그 트랜잭션을 사용하고, 없다면 트랜잭션 없이 비즈니스 로직을 수행한다.
  3. MANDATORY
    • 기존 트랜잭션이 있으면 그 트랜잭션을 사용하고, 없다면 IllegalTransactionStateException 예외를 발생시킨다.
  4. REQUIRES_NEW
    • 기존 트랜잭션은 유지시킨 채로 새로운 트랜잭션을 생성한다.
  5. NOT_SUPPORTED
    • 기존 트랜잭션이 있으면 이를 유지시킨 채로 트랜잭션 없이 비즈니스 로직을 수행한다.
  6. NEVER
    • 기존에 활성화된 트랜잭션이 있으면 IllegalTransactionStateException 예외를 발생시킨다.
  7. NESTED
    • 기존 트랜잭션이 있다면 세이브 포인트를 기록하여 예외가 발생하여도 완전히 롤백되지 않고, 세이브 포인트로 돌아갈 수 있도록 한다. 기존 트랜잭션이 없으면 REQUIRED와 같은 방식으로 동작한다.

Example

 각 전파 옵션들을 가진 ChildService를 상위의 트랜잭션을 가진 ParentService에서 호출하는 방식으로 코드를 작성하여 테스트를 해보았다. 트랜잭션이 어떻게 만들어지는 지는 DB 로그를 통해 확인을 진행하였다.

REQUIRED

부모 트랜잭션에서는 new-user1을 생성, 자식 트랜잭션에서는 new-user2를 생성해보았다.

 

ChildService
@Transactional
void childRequired() {
  User user = new User("new-user2", "password2", "김나비");
  userRepository.save(user);
}

 

ParentService
@Transactional
public void testRequired() {
  User user = new User("new-user1", "password1", "김영철");
  userRepository.save(user);

  childService.childRequired();
}

 

메서드 실행 결과 

auto commit 세팅 로그를 통해 하나의 트랜잭션에서 childRequired()와 testRequired()가 수행됨을 확인할 수 있다.

SUPPORTS

DB에 미리 old-user를 세팅해둔 상태에서 자식 트랜잭션에서 old-user 엔티티를 조회하여 값을 변경해보았다.

 

ChildService
@Transactional(propagation = Propagation.SUPPORTS)
  void childSupports() {
    User user = userRepository.findById("old-user").get();
    user.changePassword("changePassword");
  }

 

1. 기존 트랜잭션이 있는 케이스

ParentService
@Transactional
public void testSupportsWithTransaction() {
  childService.childSupports();
}

 

메서드 실행 결과

기존 트랜잭션이 있으므로 그 트랜잭션을 사용하여 update를 진행한다.

 

2. 기존 트랜잭션이 없는 케이스

ParentService
public void testSupportsWithoutTransaction() {
  childService.childSupports();
}
메서드 실행 결과

기존 트랜잭션이 없으므로 트랜잭션이 없이 동작하게 되어 update 쿼리가 수행되지 않는다.

MANDATORY

자식 트랜잭션에서 new-user2를 생성해보았다.

ChildService
@Transactional(propagation = Propagation.MANDATORY)
void childMandatory() {
  User user = new User("new-user2", "password2", "김나비");
  userRepository.save(user);
}

 

1. 기존 트랜잭션이 있는 경우

ParentService
@Transactional
public void testMandatoryWithTransaction() {
  childService.childMandatory();
}
메서드 실행 결과

기존 트랜잭션을 따라가 정상적으로 insert 쿼리가 수행된다.

 

2. 기존 트랜잭션이 없는 경우

ParentService
public void testMandatoryWithoutTransaction() {
  childService.childMandatory();
}
메서드 실행 결과

기존 트랜잭션이 없으므로 IllegalTransactionStateException 예외가 발생하고 DB에 아무런 쿼리도 날라가지 않는다.

 

REQUIRES_NEW

부모 트랜잭션에서는 new-user1을 생성, 자식 트랜잭션에서는 new-user2를 생성해보았다.

ChildService
@Transactional(propagation = Propagation.REQUIRES_NEW)
void childRequiresNew() {
  User user = new User("new-user2", "password2", "김나비");
  userRepository.save(user);
}
ParentService
@Transactional
public void testRequiredNew() {
  User user = new User("new-user1", "password1", "김영철");
  userRepository.save(user);

  childService.childRequiresNew();
}
메서드 수행 결과

new-user1, new-user2를 생성하는 트랜잭션이 각각 생성됨을 확인할 수 있다. (471, 472)

 

NOT_SUPPORTED

부모 트랜잭션에서는 new-user1을 생성하고, 자식 트랜잭션에서는 DB에 있는 유저 정보를 조회하여 값을 변경해보았다.

ChildService
@Transactional(propagation = Propagation.NOT_SUPPORTED)
void childNotSupported() {
  User user = userRepository.findById("old-user").get();
  user.changePassword("changePassword");
}
ParentService
@Transactional
public void testNotSupported() {
  User user = new User("new-user1", "password1", "김영철");
  userRepository.save(user);

  childService.childNotSupported();
}
메서드 수행 결과

new-user1 생성만 진행하고 기존 유저를 조회하고 변경된 값에 대한 update 쿼리는 수행되지 않는다.

NEVER

부모 트랜잭션에서는 new-user1을 생성, 자식 트랜잭션에서는 new-user2를 생성해보았다.

ChildService
@Transactional(propagation = Propagation.NEVER)
void childNever() {
  User user = new User("new-user2", "password2", "김나비");
  userRepository.save(user);
}
ParentService
@Transactional
public void testNever() {
  User user = new User("new-user1", "password1", "김영철");
  userRepository.save(user);

  childService.childNever();
}
메서드 수행 결과

- 부모 메서드에서 자식 메서드를 호출하자 IllegalTransactionStateException예외가 발생한다.

- DB 로그에서도 부모 트랜잭션이 수행되고 자식 트랜잭션을 생성하기 전에 예외로 인한 ROLLBACK이 진행된다.

NESTED

자식 트랜잭션에서 예외 발생 시에 세이브 포인트가 정상적으로 수행되는지 확인해보기 위해 Unique Key로 설정한 컬럼에 동일한 값을 넣었다.

ChildService
@Transactional(propagation = Propagation.NESTED)
void childNestedThrowException() {
  User user = new User("new-user2", "password2", "김춘배"); // 기존 DB unique key 중복. 예외 발생
  userRepository.save(user);
}
ParentService
@Transactional
public void testNestedThrowException() {
  User user = new User("new-user1", "password1", "김영철");
  userRepository.save(user);

  childService.childNestedThrowException();
}
메서드 수행 결과

?!?!?! 부모 트랜잭션에 세이브 포인트가 동작하지 않고 그냥 롤백되버렸다. 왜그런걸까?

  • JPA를 사용하는 경우에 savepoint 기능이 사용 불가한가보다..
Comments