본문 바로가기

PROGRAMMING/SPRING

[SPRING] 어떤 @Transactional을 써야할까?

@Transactional은 크게 자카르타와 스프링 두 가지 방식이 있다.

 

1. 스프링에서 제공하는 Transactional (패키지: org.springframework.transaction.annotation.Transactional)

 

스프링에서 제공하는 Spring AOP를 사용하여 트랜잭션을 관리한다.

스프링의 트랜잭션 관리 방식은 매우 유연하며, Propagation, Isolation, Timeout, ReadOnly 등을 커스터마이징할 수 있는데

 

아래 코드는 스프링에서 제공하는 Transactional의 다양한 커스터마이징 사용 예제이다.

 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionalService {

    @Autowired
    private ExampleRepository exampleRepository;

    // Propagation: 기존 트랜잭션이 있어도 새로운 트랜잭션을 시작 (REQUIRES_NEW)
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void performTransactionWithPropagation() {
        // 비즈니스 로직 - 새로운 트랜잭션이 시작됨
        exampleRepository.save(new ExampleEntity("Propagation Test"));
    }

    // Isolation: 트랜잭션 격리 수준을 가장 높은 수준인 SERIALIZABLE로 설정
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public void performTransactionWithIsolation() {
        // 격리 수준이 적용된 트랜잭션 내에서 비즈니스 로직 실행
        exampleRepository.save(new ExampleEntity("Isolation Test"));
    }

    // Timeout: 트랜잭션 제한 시간을 5초로 설정
    @Transactional(timeout = 5)
    public void performTransactionWithTimeout() throws InterruptedException {
        // 6초간 대기하여 제한 시간 5초를 초과하도록 설정 (트랜잭션 실패 유도)
        Thread.sleep(6000);  
        exampleRepository.save(new ExampleEntity("Timeout Test"));
    }

    // Read-only: 읽기 전용 트랜잭션을 지정하여 데이터 변경 방지
    @Transactional(readOnly = true)
    public ExampleEntity performTransactionWithReadOnly() {
        // 데이터베이스에서 조회만 수행하고 수정 불가
        return exampleRepository.findById(1L).orElse(null);
    }

    // Rollback for: 특정 예외(CustomException)가 발생하면 트랜잭션 롤백
    @Transactional(rollbackFor = CustomException.class)
    public void performTransactionWithRollbackFor() throws CustomException {
        // 데이터 저장
        exampleRepository.save(new ExampleEntity("RollbackFor Test"));
        // 예외 발생으로 트랜잭션 롤백
        if (true) {
            throw new CustomException("롤백 트리거");
        }
    }
}

 

기본 설정은 아래와 같다.

  • Propagation.REQUIRED: 현재 트랜잭션이 있으면 그 트랜잭션에 참여하고, 없으면 새 트랜잭션을 생성.
  • Isolation.DEFAULT: 데이터베이스의 기본 격리 수준을 따름.
  • Timeout.DEFAULT: 제한 시간이 설정되지 않음.
  • Read-only = false: 트랜잭션이 읽기/쓰기를 모두 허용.
  • Rollback: RuntimeException이나 Error가 발생하면 트랜잭션이 롤백됨.

 

2. 자카르타 EE에서 지원하는 Transactional (패키지 : jakarta.transaction.Transactional)

Spring AOP가 아닌, 자카르타 EE의 기본 트랜잭션 관리 메커니즘을 따르며 스프링과 달리 트랜잭션 전파한다.

컨테이너(자카르타 EE 서버)가 트랜잭션 관리의 주체라고 볼 수 있다.

단점은 격리 수준 등의 커스터마이징 기능이 제한적이라는 것. 하지만 자카르타 EE 표준이기 때문에 스프링 외의 다른 EE 환경에서도 사용이 가능하다.

즉, 프레임워크 간 호환성을 고려할 때 적합하다.

 

구현 예시

import jakarta.transaction.Transactional;

@Transactional(rollbackOn = Exception.class)
public void performJakartaTransaction() {
    // 자카르타 트랜잭션 관리하에 실행
}

 

 

사실 궁금한건 이거였다.

혹시라도 한 프로젝트에서 두 방식을 섞어서 개발하면 문제가 생길 수도 있지않을까?

 

결론적으로 

스프링의 @Transactional을 기본 설정으로 사용하는 경우, 대부분의 상황에서는 문제 없이 트랜잭션 관리가 잘 동작한다.

하지만 옵션을 커스터마이징하여 사용하는 경우특정 조건에서는 몇 가지 문제점이나 주의할 점이 있다.

 

  1. 트랜잭션 관리 중복: 스프링과 자카르타 트랜잭션 관리 방식이 동시에 적용되면 중복 관리로 인해 트랜잭션이 두 번 시작되거나 예기치 않게 종료될 수 있음.
  2. 전파 및 격리 수준 충돌: 두 트랜잭션의 Propagation 및 Isolation Level이 상충될 수 있어, 예상치 못한 트랜잭션 처리 결과가 발생할 수 있음.
  3. 예외 및 롤백 처리 차이: 스프링과 자카르타 트랜잭션에서 예외에 대한 롤백 처리 방식이 다를 수 있으며, 일관성이 없는 트랜잭션 롤백이 발생할 수 있음.
  4. 트랜잭션 관리자의 일관성 부족: 스프링 트랜잭션 관리자와 자카르타 트랜잭션 관리자가 동일한 트랜잭션을 처리할 때 동기화 문제가 발생할 수 있음.

 

 

요약하면 아래와 같다.

    • 자카르타 트랜잭션은 표준 사양으로, 주로 자카르타 EE 또는 Java EE 환경에서 사용되며, 단순한 트랜잭션 관리를 제공하며
      특정 프레임워크에 종속되지 않고 표준적인 트랜잭션 처리를 원하는 경우에 사용
    • 스프링 트랜잭션이 더 세부적인 트랜잭션 제어가 가능해 복잡한 애플리케이션에서 유용하지만, 스프링 프레임워크에서만 동작
    • 두개를 하나의 프로젝트에서 섞어써도 대부분의 상황에서는 문제가 되지 않지만 권장되지 않는다.
    • 두 트랜잭션 관리 메커니즘이 충돌할 수 있으며, 트랜잭션의 일관성이 보장되지 않을 가능성이 높기 때문에
      한 프로젝트에서는 하나의 트랜잭션 관리 방식만 사용하는 것이 권장된다.