@Transactional은 Spring에서 트랜잭션 관리를 제공하는 핵심 애너테이션이다.
여러 로직을 작성하다 보면 기존 트랜잭션이 진행 중일때, 트랜잭션을 추가해야 하는 경우가 있다. 이러한 사항을 결정하는 것으로 @Transactional의 옵션인 propagation이 있는데, 트랜잭션이 서로 어떻게 상호작용할지 정의하는 중요한 설정이다.
트랜잭션 전파(Propagation)는 메서드 호출 시 현재 활성화된 트랜잭션이 존재하는지, 새로운 트랜잭션을 생성해야 하는지를 결정한다.
SpringBoot 3.4.0
JDK 17
@Transactional 애너테이션의 옵션에서 제공하는 Propagation의 종류는 7가지로 구성되어있다.
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
이 중에서 default값인 REQUIRED와 REQUIRES_NEW에 대해서는 조금 상세히 알아보도록 한다.
default 옵션으로 대부분의 일반적인 비즈니스 로직에서 사용된다.
현재 활성화된 트랜잭션이 있으면 이를 사용하고, 없으면 새로운 트랜잭션을 생성한다.
트랜잭션이 2개로 분리된다고 하더라도 실질적인 물리 트랜잭션이 분리되는 것이 아니라, 논리 트랜잭션이 분리되어 2개가 되는 것이다.
그렇기에 커밋 자체는 각각의 논리 트랜잭션이 각각 1회씩 실행된다.
항상 새로운 트랜잭션을 시작한다. 주로 부모 트랜잭션과 독립적으로 처리해야 할 작업(로깅, 푸시 전송 등)에 사용된다.
별도의 트랜잭션(자식 트랜잭션)이 만들어지고 실행되기에, 부모 트랜잭션과는 독립적으로 커밋 혹은 롤백된다.
REQUIRED와 다르게 물리 트랜잭션 2개로 분리되는 것이기에, 남용 시 DBCP의 고갈에 대해서 주의해야 한다.
트랜잭션의 전파 옵션인 REQUIRED와 REQUIRED_NEW는 기존 트랜잭션을 사용할지, 새로운 트랜잭션을 생성하여 사용할지를 결정하는 데 큰 차이가 있음을 알 수 있었다.
아래 실제 예제 코드를 통해 이 둘의 차이를 비교해보고자 한다.
@Service
public class TransactionExampleService {
private static final Logger logger = LoggerFactory.getLogger(TransactionExampleService.class);
private final UserJpaRepository userJpaRepository;
private final AnotherService anotherService;
public TransactionExampleService(final UserJpaRepository userJpaRepository, final AnotherService anotherService) {
this.userJpaRepository = userJpaRepository;
this.anotherService = anotherService;
}
@Transactional // default: Propagation.REQUIRES
public void requiredTransaction() {
logTransaction("REQUIRED - Main");
userJpaRepository.findAll();
anotherService.methodWithRequired(); // Propagation.REQUIRED
anotherService.methodWithRequiresNew(); // Propagation.REQUIRES_NEW
}
private void logTransaction(final String context) {
final String txName = TransactionSynchronizationManager.getCurrentTransactionName();
final boolean isTxActive = TransactionSynchronizationManager.isActualTransactionActive();
logger.info("[{}] Transaction Active: {}, Transaction Name: {}", context, isTxActive, txName);
}
}
@Service
public class AnotherService {
private static final Logger logger = LoggerFactory.getLogger(AnotherService.class);
private final UserJpaRepository userJpaRepository;
public AnotherService(final UserJpaRepository userJpaRepository) {
this.userJpaRepository = userJpaRepository;
}
@Transactional(propagation = Propagation.REQUIRED)
public void methodWithRequired() {
logTransaction("REQUIRED - Sub");
userJpaRepository.findAll();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodWithRequiresNew() {
logTransaction("REQUIRES_NEW - Sub");
userJpaRepository.findAll();
}
private void logTransaction(final String context) {
final String txName = TransactionSynchronizationManager.getCurrentTransactionName();
final boolean isTxActive = TransactionSynchronizationManager.isActualTransactionActive();
logger.info("[{}] Transaction Active: {}, Transaction Name: {}", context, isTxActive, txName);
}
}
@SpringBootApplication
public class TransactionExampleApplication {
public static void main(String[] args) {
final ConfigurableApplicationContext context = SpringApplication.run(TransactionExampleApplication.class, args);
final TransactionExampleService service = context.getBean(TransactionExampleService.class);
service.requiredTransaction();
}
}
[REQUIRED - Main] Transaction Active: true, Transaction Name: com.example.transactionexample.service.TransactionExampleService.requiredTransaction
[REQUIRED - Sub] Transaction Active: true, Transaction Name: com.example.transactionexample.service.TransactionExampleService.requiredTransaction
[REQUIRES_NEW - Sub] Transaction Active: true, Transaction Name: com.example.transactionexample.service.AnotherService.methodWithRequiresNew
REQUIRED - Main에서 시작된 트랜잭션은 REQUIRED - Sub에서도 동일한 트랜잭션을 공유REQUIRES_NEW - Sub는 새로운 트랜잭션을 생성됨을 확인REQUIRED _NEW는 항상 새로운 트랜잭션을 생성하기에, 트랜잭션 생성과 관리에 따른 오버헤드가 발생한다.REQUIRED_NEW에서 결제 기록이 저장되면, 비즈니스 로직상 데이터 일관성이 깨질 수 있다.REQUIRED_NEW를 사용할 경우에 주요 트랜잭션은 영향을 받지 않아야 한다.REQUIRED_NEW 내부에서 발생한 예외는 상위 트랜잭션으로 전파되지 않도록 try-catch로 처리한다.비동기 처리를 하여 주요 트랜잭션과 분리할 수 있다.NESTED와 REQUIRED는 모두 기존 트랜잭션이 존재하면 해당 트랜잭션에 참여하지만, 트랜잭션 롤백 처리 방식과 Savpoint 생성 여부에서 차이가 있다.
| 특징 | NESTED | REQUIRED |
|---|---|---|
| 부분 롤백 가능 여부 | Savepoint를 이용하여 부분 롤백 사용 가능 | 상위 트랜잭션에 종속되기에 상위 트랜잭션이 롤백되면 모두 롤백 |
| 상위 트랜잭션과의 관계 | 상위 트랜잭션 내에서 동작하되, 부분적으로 독립적인 제어 가능 | 상위 트랜잭션과 동일한 경계를 공유 |
상위 트랜잭션의 일부로 동작하지만, 부분적으로 롤백할 필요가 있는 경우
상위 트랜잭션과 동일한 경계를 공유하여 작업을 하나로 묶어야 하는 경우
https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html
https://mangkyu.tistory.com/269