Chris Devlog

선언적 트랜잭션(@Transactional)의 트랜잭션 전파속성 본문

Spring Boot

선언적 트랜잭션(@Transactional)의 트랜잭션 전파속성

Chris Dev Heo 2022. 9. 23. 04:12
반응형

선언적 트랜잭션


AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해 주고 속성을 지정할 수 있게 하는 방법

@Slf4j
@Service
public class UserServiceImpl implements UserService {

 @Transactional
 public void add(){
   try{
     ...
   } catch(Exception e) {
     log.error(e.getMessage(), e);
   }
 }
}

 

프로그램에 의한 트랜잭션


TransactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 사용해 직접 코드안에서 사용하는 방법

@Slf4j
@Service
public class UserServiceImpl implements UserService {
 
 private PlatformTransactionManager transactionManager;
 
 @Autowired
  UserServiceImpl(PlatformTransactionManager transactionManager) {
    this.transactionManager = transactionManager;
  }
 
 public add(){
   TransactionStatus jobStatus = transactionManager
   	.getTransaction(new DefaultTransactionDefinition());
   try{
   	  ...
   } catch(Exception e) {
     jobStatus.setRollbackOnly();
     log.error(e.getMessage(), e);
   } finally {
     if (jobStatus.isRollbackOnly()) {
       transactionManager.rollback(jobStatus);
     } else {
       transactionManager.commit(jobStatus);
     }
   }
 }
}

 

트랜잭션 전파


트랜잭션의 경계에서 이미 진행중인 트랜잭션이 있을때 또는 없을때 어떻게 동작할 것인가를 결정하는 방식

트랜잭션 전파속성


트랜잭션 전파

위 그림처럼 각각 독립적인 트랜잭션 경계를 가진 두개의 코드에서 A의 트랜잭션이 시작되고 B를 호출 했을 때, 이미 진행중인 트랜잭션(A의 트랜잭션)이 어떻게 영향을 미칠 수 있는가를 정의 하는것

 

  1. REQUIRED
    • 기본 설정값
    • 이미 시작된 트랜잭션이 있으면 참여
    • 시작된 트랜잭션이 없으면 새 트랜잭션 생성
  2. SUPPORTS
    • 이미 시작된 트랜잭션이 있으면 참여
    • 시작된 트랜잭션이 없으면 비 트랜잭션으로 실행
  3. MANDATORY
    • 이미 시작된 트랜잭션이 있으면 참여
    • 시작된 트랜잭션이 없는 경우 예외를 발생
  4. REQUIRES_NEW
    • 이미 시작된 트랜잭션이 있어도 새 트랜잭션 생성
    • 시작된 트랜잭션이 있으면 일시 중단
  5. NOT_SUPPORTED
    • 이미 시작된 트랜잭션이 있으면 일시 중단 
    • 비 트랜잭션으로 실행
  6. NEVER
    • 비 트랜잭션으로 실행
    • 트랜잭션이 있으면 예외를 발생
  7. NESTED
    • 중첩(자식) 트랜잭션을 커밋/중단해도 상위 트랜잭션의 상태에는 영향을 미치지 않음
    • 상위(부모) 트랜잭션이 중단되면 중첩(자식) 트랜잭션도 중단
    • 상위(부모) 트랜잭션은 하위 트랜잭션이 수정한 내용확인 가능
    • 중첩 트랜잭션은 부모가 커밋할 때까지 부모 트랜잭션에 의해 유지되며, 부모가 커밋하면 자식 트랜잭션이 수행한 모든 수정 사항도 커밋
    • 아쉽게도 JPA(Hibernate 구현체)에서 중첩 트랜잭션 사용시 오류 발생
    • 현업에서 JPA를 사용중이므로 오류 테스트만 진행

 

Test


REQUIRED

  • 이미 시작된 트랜잭션이 있으면 참여
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  @Transactional
  public void requiredAtoB() {
    log.info("AService Transaction active is {}", TransactionSynchronizationManager.isActualTransactionActive());
    bService.noneTransactional();
  }
}

@Slf4j
@Service
public class BService {

  public void noneTransactional() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.requiredAtoB]
AService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.requiredAtoB] active is [true]
BService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.requiredAtoB] active is [true]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.requiredAtoB]
  • 시작된 트랜잭션이 없으면 새 트랜잭션 생성
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  public void requiredB() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.required();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.REQUIRED)
  public void required() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
AService Transaction name [null] active is [false]
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.required]
BService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.BService.required] active is [true]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.required]

 

SUPPORTS

  • 이미 시작된 트랜잭션이 있으면 참여
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  @Transactional
  public void supportsAtoB() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.supports();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.SUPPORTS)
  public void supports() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.supportsAtoB]
AService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.supportsAtoB] active is [true]
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.supports]
BService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.supportsAtoB] active is [true]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.supports]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.supportsAtoB]

 

  • 시작된 트랜잭션이 없는경우 비 트랜잭션으로 실행
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  public void supportsB() {
    log.info("AService Transaction active is {}",
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.supports();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.SUPPORTS)
  public void supports() {
    log.info("BService Transaction active is {}",
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
AService Transaction name [null] active is [false]
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.supports]
BService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.BService.supports] active is [false]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.supports]

 

Mandatory

  • 이미 시작된 트랜잭션이 있으면 참여
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  @Transactional
  public void mandatoryAtoB() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.mandatory();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.MANDATORY)
  public void mandatory() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.mandatoryAtoB]
AService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.mandatoryAtoB] active is [true]
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.mandatory]
BService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.mandatoryAtoB] active is [true]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.mandatory]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.mandatoryAtoB]
  • 시작된 트랜잭션이 없는 경우 예외를 발생
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  public void mandatoryB() {
    log.info("AService Transaction active is {}",
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.mandatory();
  }
}

@Slf4j
@Service
public class BService {

  public void mandatoryB() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.mandatory();
  }
}

// Console
AService Transaction name [null] active is [false]
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'] with root cause

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

 

REQUIRES_NEW

  • 이미 시작된 트랜잭션이 있어도 새 트랜잭션 생성하며, 시작된 트랜잭션이 있으면 일시 중단
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  @Transactional
  public void requiresNew() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.requiresNew();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.REQUIRES_NEW)
  public void requiresNew() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.requiresNew]
AService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.requiresNew] active is [true]
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.requiresNew]
BService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.BService.requiresNew] active is [true]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.requiresNew]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.requiresNew]

 

NOT_SUPPORTED

  • 이미 시작된 트랜잭션이 있으면 일시 중단 -> 비 트랜잭션으로 실행
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  @Transactional
  public void notSupported() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.notSupported();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.NOT_SUPPORTED)
  public void notSupported() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.notSupported]
AService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.notSupported] active is [true]
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.notSupported]
BService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.BService.notSupported] active is [false]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.BService.notSupported]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.notSupported]

 

 

NEVER

  • 비 트랜잭션으로 실행 -> 트랜잭션이 있으면 예외를 발생
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  @Transactional
  public void never() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.never();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.NEVER)
  public void never() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
Getting transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.never]
AService Transaction name [com.chirs.mybook.domain.transaction_propagation.service.AService.never] active is [true]
Completing transaction for [com.chirs.mybook.domain.transaction_propagation.service.AService.never] after exception: org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'] with root cause

 

 

NESTED 

  • 현업에서 JPA를 사용중이므로 오류 테스트만 진행
@Slf4j
@Service
@RequiredArgsConstructor
public class AService {

  private final BService bService;

  @Transactional
  public void nested() {
    log.info("AService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
    bService.nested();
  }
}

@Slf4j
@Service
public class BService {

  @Transactional(propagation = Propagation.NESTED)
  public void nested() {
    log.info("BService Transaction name [{}] active is [{}]",
        TransactionSynchronizationManager.getCurrentTransactionName(),
        TransactionSynchronizationManager.isActualTransactionActive());
  }
}

// Console
NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities

 

참조


https://docs.oracle.com/cd/E17276_01/html/gsg_xml_txn/java/nestedtxn.html

 

Nested Transactions

A nested transaction is used to provide a transactional guarantee for a subset of operations performed within the scope of a larger transaction. Doing this allows you to commit and abort the subset of operations independently of the larger transaction. The

docs.oracle.com

https://reiphiel.tistory.com/entry/understanding-of-spring-transaction-management-practice

 

Spring 트랜잭션 관리의 이해 - 실전편

Spring 트랜잭션 관리방법 Spring(스프링)에서 트랜잭션(Transaction)을 관리하는 방법은 크게 서로 대비되는 2가지 방법으로 나눌 수 있습니다. 프로그램에 의한(Programmatic) 트랜잭션 관리 첫번째로 알

reiphiel.tistory.com

토비의 스프링 3.1 - 6.6 트랜잭션 속성 ~

반응형