@Transaction의 Propagation 옵션

ifi9·2024년 11월 17일

@Transactional은 Spring에서 트랜잭션 관리를 제공하는 핵심 애너테이션이다.
여러 로직을 작성하다 보면 기존 트랜잭션이 진행 중일때, 트랜잭션을 추가해야 하는 경우가 있다. 이러한 사항을 결정하는 것으로 @Transactional의 옵션인 propagation이 있는데, 트랜잭션이 서로 어떻게 상호작용할지 정의하는 중요한 설정이다.

트랜잭션 전파(Propagation)

트랜잭션 전파(Propagation)는 메서드 호출 시 현재 활성화된 트랜잭션이 존재하는지, 새로운 트랜잭션을 생성해야 하는지를 결정한다.

실행 환경

SpringBoot 3.4.0
JDK 17

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;
    }
}

이 중에서 default값인 REQUIREDREQUIRES_NEW에 대해서는 조금 상세히 알아보도록 한다.

1. REQUIRED (default)

default 옵션으로 대부분의 일반적인 비즈니스 로직에서 사용된다.
현재 활성화된 트랜잭션이 있으면 이를 사용하고, 없으면 새로운 트랜잭션을 생성한다.
트랜잭션이 2개로 분리된다고 하더라도 실질적인 물리 트랜잭션이 분리되는 것이 아니라, 논리 트랜잭션이 분리되어 2개가 되는 것이다.
그렇기에 커밋 자체는 각각의 논리 트랜잭션이 각각 1회씩 실행된다.

  • 물리 트랜잭션: 실제 DB의 트랜잭션
  • 논리 트랜잭션: Spring의 TransactionManager에서 관리되는 트랜잭션

2. REQUIRES_NEW

항상 새로운 트랜잭션을 시작한다. 주로 부모 트랜잭션과 독립적으로 처리해야 할 작업(로깅, 푸시 전송 등)에 사용된다.
별도의 트랜잭션(자식 트랜잭션)이 만들어지고 실행되기에, 부모 트랜잭션과는 독립적으로 커밋 혹은 롤백된다.
REQUIRED와 다르게 물리 트랜잭션 2개로 분리되는 것이기에, 남용 시 DBCP의 고갈에 대해서 주의해야 한다.

3. REQUIRED와 REQUIRED_NEW 비교

트랜잭션의 전파 옵션인 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

  • REQUIRED - Main에서 시작된 트랜잭션은 REQUIRED - Sub에서도 동일한 트랜잭션을 공유
  • 결과적으로 두 메서드는 같은 트랜잭션 경계 내에서 실행

REQUIRES_NEW

  • REQUIRES_NEW - Sub는 새로운 트랜잭션을 생성됨을 확인
  • 새로운 트랜잭션은 독립적으로 실행

4. 나머지 트랜잭션 전파 속성

SUPPORTS

  • 기존 트랜잭션이 있으면 이를 사용하고, 없으면 트랜잭션 없이 실행
  • 트랜잭션 유무와 관계없이 실행

NOT_SUPPORTED

  • 현재 활성화된 트랜잭션이 있으면 이를 일시 정지하고, 트랜잭션 없이 실행
  • 트랜잭션 환경과 분리

MANDATORY

  • 반드시 기존 트랜잭션 내에서 실행되어야 함. 트랜잭션이 없으면 예외를 발생시킴
  • 상위 트랜잭션이 있어야 하는 메서드(하위 로직 호출 등)
  • 트랜잭션 없이 호출 시 IllegalTransactionStateException 예외 발생

NEVER

  • 트랜잭션 내에서 실행되면 안 됨. 트랜잭션이 활성화된 상태라면 예외를 발생시킴
  • 트랜잭션 없이 처리해야 하는 메서드
  • 트랜잭션 환경을 강제로 제거

NESTED

  • 기존 트랜잭션이 있으면 중첩된 트랜잭션을 생성하고, 없으면 새로운 트랜잭션을 생성
  • 부모 트랜잭션 내에서 특정 작업을 별도로 롤백하거나 커밋해야 할 경우
  • Savepoint를 생성하여 부분 롤백이 가능

5. REQUIRED_NEW 사용 시 고려할 점

성능 문제

  • REQUIRED _NEW는 항상 새로운 트랜잭션을 생성하기에, 트랜잭션 생성과 관리에 따른 오버헤드가 발생한다.
  • 또한 DBCP의 부족으로 인해 성능이 저하될 수 있다.

데이터 일관성 문제

  • 상위 트랜잭션이 롤백되었는데, 하위 REQUIRED_NEW에서 결제 기록이 저장되면, 비즈니스 로직상 데이터 일관성이 깨질 수 있다.
    • 하위 트랜잭션이 반드시 성공해야 할 작업이라면, 보상 트랜잭션 혹은 SAGA 패턴을 고려해본다.

로깅 작업을 할 경우

  • 로깅이 아니라더라도, 부가적인 작업에서 REQUIRED_NEW를 사용할 경우에 주요 트랜잭션은 영향을 받지 않아야 한다.
  • REQUIRED_NEW 내부에서 발생한 예외는 상위 트랜잭션으로 전파되지 않도록 try-catch로 처리한다.
  • 혹은 로깅과 같은 작업은 굳이 트랜잭션에 묶을 필요는 없기에 비동기 처리를 하여 주요 트랜잭션과 분리할 수 있다.

6. NESTED와 REQUIRED

차이점

NESTEDREQUIRED는 모두 기존 트랜잭션이 존재하면 해당 트랜잭션에 참여하지만, 트랜잭션 롤백 처리 방식과 Savpoint 생성 여부에서 차이가 있다.

특징NESTEDREQUIRED
부분 롤백 가능 여부Savepoint를 이용하여 부분 롤백 사용 가능상위 트랜잭션에 종속되기에 상위 트랜잭션이 롤백되면 모두 롤백
상위 트랜잭션과의 관계상위 트랜잭션 내에서 동작하되, 부분적으로 독립적인 제어 가능상위 트랜잭션과 동일한 경계를 공유

사용 사례

NESTED

상위 트랜잭션의 일부로 동작하지만, 부분적으로 롤백할 필요가 있는 경우

REQUIRED

상위 트랜잭션과 동일한 경계를 공유하여 작업을 하나로 묶어야 하는 경우

참고 자료

https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-propagation.html
https://mangkyu.tistory.com/269

0개의 댓글