@Transactional propagation
@Trasactional 어노테이션의 propagation에 대해 알아보고 중첩된 트랜잭션을 사용하게 될 때 생기는 문제에 대해 알아보겠습니다.
@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;
}
}
Propagtaion 속성
REQUIRED -
기본 속성으로 부모 트랜잭션이 있다면 기존 트랜잭션을 사용합니다.
REQUIRES_NEW -
항상 새로운 트랜잭션을 사용합니다. 이미 진행중인 트랜잭션이 있다면 보류하고 해당 트랜잭션을 먼저 진행합니다.
SUPPORTS -
부모 트랜잭션이 있다면 기존 트랜잭션을 사용하고 없으면 트랜잭션 없이 실행됩니다.
NOT_SUPPORTED -
부모 트랜잭션이 있으나 없으나 트랜잭션 없이 실행됩니다.
MANDATORY -
부모 트랜잭션 내에서 실행되며, 활성화된 트랜잭션이 없을 경우 Exception이 발생합니다.
NESTED -
부모 트랜잭션이 커밋될 때 같이 커밋, 롤백이 부모 트랜잭션에 영향을 미치지 않습니다.
NEVER -
트랜잭션 없이 실행되며 부모 트랜잭션이 존재하면 Exception이 발생합니다.
테스트 코드
각 내용이 맞는지 확인하기 위해 테스트 코드를 작성합니다.
클래스는 부모 역할을 할 PropagationService.java 와 자식 역할을 할 PropagationInternalService.java로 분리합니다.
각 서비스의 메소드에서는 데이터 저장하는 로직을 각각 추가합니다.
자식 트랜잭션의 오류에 대해 어떻게 처리되는지 확인해 보겠습니다.
PropagationService.java
package com.infitry.laboratory.service.transaction.propagation;
import com.infitry.laboratory.entity.Member;
import com.infitry.laboratory.persistence.jpa.MemberRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PropagationService {
private final MemberRepository memberRepository;
private final PropagationInternalService propagationInternalService;
@Transactional
public void requiredTransaction() {
saveNewMember();
try {
propagationInternalService.required();
} catch(RuntimeException e) {
printLog(e);
}
}
@Transactional
public void requiresNewTransaction() {
saveNewMember();
try {
propagationInternalService.requiresNew();
} catch(RuntimeException e) {
printLog(e);
}
}
@Transactional
public void supportsTransaction() {
saveNewMember();
try {
propagationInternalService.supports();
} catch(RuntimeException e) {
printLog(e);
}
}
@Transactional
public void notSupportedTransactional() {
saveNewMember();
try {
propagationInternalService.notSupported();
} catch(RuntimeException e) {
printLog(e);
}
}
@Transactional
public void nestedTransactional() {
saveNewMember();
try {
propagationInternalService.nested();
} catch(RuntimeException e) {
printLog(e);
}
}
@Transactional
public void mandatoryTransactional() {
saveNewMember();
try {
propagationInternalService.mandatory();
} catch(RuntimeException e) {
printLog(e);
}
}
@Transactional
public void neverTransactional() {
saveNewMember();
propagationInternalService.never();
}
private void saveNewMember() {
Member member = new Member();
member.setName("111");
member.setNickName("32132");
memberRepository.save(member);
}
private static void printLog(Exception e) {
log.info("Exception 을 외부로 throw 하지 않는다.", e);
}
}
PropagationInternalService.java
package com.infitry.laboratory.service.transaction.propagation;
import com.infitry.laboratory.entity.Order;
import com.infitry.laboratory.persistence.jpa.OrderRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static org.springframework.transaction.annotation.Propagation.*;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class PropagationInternalService {
private final OrderRepository orderRepository;
@Transactional(propagation = REQUIRED)
public void required() {
log.info("활성된 트랜잭션이 있다면 기존 트랜잭션을 사용합니다.");
saveNewOrder();
}
@Transactional(propagation = REQUIRES_NEW)
public void requiresNew() {
log.info("항상 새로운 트랜잭션을 사용합니다. 이미 진행중인 트랜잭션이 있다면 보류하고 해당 트랜잭션을 먼저 진행합니다.");
saveNewOrder();
}
@Transactional(propagation = SUPPORTS)
public void supports() {
log.info("활성된 트랜잭션이 있다면 기존 트랜잭션을 사용하고 없으면 트랜잭션 없이 실행합니다.");
saveNewOrder();
}
@Transactional(propagation = NOT_SUPPORTED)
public void notSupported() {
log.info("활성화 트랜잭션이 있던 없던 Transaction 없이 실행합니다.");
saveNewOrder();
}
@Transactional(propagation = MANDATORY)
public void mandatory() {
log.info("부모 트랜잭션 내에서 실행되며, 부모 트랜잭션이 없을 경우 Exception 이 발생합니다.");
saveNewOrder();
}
@Transactional(propagation = NESTED)
public void nested() {
log.info("부모 트랜잭션이 커밋될 때 같이 커밋, 자식 트랜잭션의 롤백은 부모 트랜잭션에 영향이 없습니다.");
saveNewOrder();
}
@Transactional(propagation = NEVER)
public void never() {
log.info("트랜잭션 없이 실행되며 부모 트랜잭션이 존재하면 Exception 이 발생합니다.");
saveNewOrder();
}
private void saveNewOrder() {
Order order = new Order();
order.setOrderName("신규주문-1");
orderRepository.save(order);
throw new RuntimeException("rollback");
}
}
PropagationServiceTest.java
package com.infitry.laboratory.service.transaction.propagation;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class PropagationServiceTest {
@Autowired
PropagationService propagationService;
@Test
@DisplayName("required 테스트")
public void required() {
propagationService.requiredTransaction();
}
@Test
@DisplayName("requires new 테스트")
public void requiresNew() {
propagationService.requiresNewTransaction();
}
@Test
@DisplayName("supports 테스트")
public void supports() {
propagationService.supportsTransaction();
}
@Test
@DisplayName("not Supported 테스트")
public void notSupported() {
propagationService.notSupportedTransactional();
}
@Test
@DisplayName("nested 테스트")
public void nested() {
propagationService.nestedTransactional();
}
@Test
@DisplayName("mandatory 테스트")
public void mandatory() {
propagationService.mandatoryTransactional();
}
@Test
@DisplayName("never 테스트")
public void never() {
propagationService.neverTransactional();
}
}
Repository, Member, Order Entity 등은 간단하게 작성되어 있습니다.
전체 코드는 다음 리파지토리에서 확인하실 수 있습니다. (Github)
테스트
1. REQUIRED
부모 트랜잭션을 사용합니다.
테스트를 실행하면 다음과 같은 오류가 발생합니다.
Transaction silently rolled back because it has been marked as rollback-only
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
이상합니다. 분명 PropagationInternalService의 메서드는 Exception을 catch 하였는데 어째서 오류가 발생한 걸까요?
REQUIRED는 부모 트랜잭션을 그대로 사용하게 됩니다. 동일 트랜잭션을 사용하다 보니 자식트랜잭션에서 발생한 예외에 대해 트랜잭션에 rollback-only 가 마크되었고 동일 트랜잭션을 사용하는 부모 트랜잭션에서 커밋을 할 때 이미 rollback-only 마크가 되어 롤백되었습니다.
그럼 자식 트랜잭션에서 예외를 throw 하지 않게 하면 어떻게 될까요?
@Transactional(propagation = REQUIRED)
public void required() {
try {
log.info("활성된 트랜잭션이 있다면 기존 트랜잭션을 사용합니다.");
saveNewOrder();
} catch (Exception e) {
log.error("error - ", e);
}
}
코드를 수정했습니다. 자식 트랜잭션에서 더 이상 예외를 외부로 던지지 않습니다.
로그로 예외를 확인할 수 있고 테스트는 통과합니다.
2. REQUIRES_NEW
항상 새로운 트랜잭션을 사용합니다.
테스트를 실행하면?
새로운 트랜잭션을 생성해서 처리하기 때문에 앞서 발생한 UnexpectedRollbackException은 발생하지 않습니다.
3. SUPPORTS
부모 트랜잭션이 있으면 사용하고 없으면 트랜잭션 없이 실행합니다.
부모 트랜잭션을 사용하였고 rollback-only 마크가 되었기 때문에 마찬가지로 UnexpectedRollbackException이 발생합니다. 부모 트랜잭션이 없다면? 당연히 발생하지 않습니다.
4. NOT_SUPPORTED
부모 트랜잭션과 상관없이 트랜잭션 없이 실행합니다.
당연히? 성공합니다.
5. NESTED
부모 트랜잭션이 커밋될 때 같이 커밋, 자식 트랜잭션의 롤백은 부모 트랜잭션에 영향이 없습니다.
테스트 시 다음과 같은 예외가 발생합니다.
org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities
JPA에서는 savepoints를 지원하지 않아 예외가 발생합니다.
6. MANDATORY
부모 트랜잭션 내에서 실행되며, 부모 트랜잭션이 없을 경우 Exception이 발생합니다.
부모 트랜잭션이 존재할 때는 마찬가지로 UnexpectedRollbackException이 발생합니다.
부모 트랜잭션이 존재하지 않을 때는?
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
다음과 같은 예외가 발생합니다.
7. NEVER
트랜잭션 없이 실행되며 부모 트랜잭션이 존재하면 Exception이 발생합니다.
부모 트랜잭션이 존재할 때 다음과 같은 예외가 발생합니다.
Existing transaction found for transaction marked with propagation 'never'
org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
부모 트랜잭션이 존재하지 않는다면? 기존 예외가 발생합니다.
정리
@Transactional의 propagation 속성에 대해 알아보았습니다.
각 속성에 따른 자식 트랜잭션의 롤백이 부모트랜잭션에 미치는 영향도도 알아보았습니다.
실무에서 여러 복잡한 로직들을 다룰 때 항상 트랜잭션을 확인하는 습관이 필요하다는 걸 다시 한번 더 느꼈습니다.😀😀