빙응의 공부 블로그

[Spring]스프링의 트랜잭션 전파 속성 본문

Spring/개인공부_이론

[Spring]스프링의 트랜잭션 전파 속성

빙응이 2024. 12. 3. 20:43

 

📝 스프링 트랜잭션 전파 속성

 

트랜잭션 전파 속성은
트랜잭션이 진행중일 때 추가 트랜잭션 진행을 어떻게 하는지 결정하는 것입니다.

 

그렇다면 트랜잭션 전파 속성을 어떨 때 사용하는 것일까요?

 

아래 코드처럼 propagation을 통해서 설정할 수 있습니다. 

    @Transactional
    public void acceptRequest(Long fromMemberId, Long toMemberId)
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void acceptRequest(Long fromMemberId, Long toMemberId)

 

 

트랜잭션 전파 속성은 다른 메소드나 서비스 호출 시, 트랜잭션을 어떻게 다룰지 결정합니다. 

그렇다면 왜 트랜잭션을 다룰까요? 그 이유는 트랜잭션의 시작과 종료에 있습니다.

 

 

 

트랜잭션의 시작

 

트랜잭션은 PlatformTransactionManager를 통해 직접 사용할 수 있습니다.

트랜잭션은 데이터베이스 커넥션 하나를 가져와 사용하는 경우가 트랜잭션입니다.  스프링은 내부적으로 커넥션을 갖고 추상화된 트랜잭션 매니저를 이용합니다. 이때 다양한 설정을 트랜잭션 매니저 내부에서 진행합니다.

 

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void performTransaction() {
        // 트랜잭션 정의
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setIsolationLevel(DefaultTransactionDefinition.ISOLATION_READ_COMMITTED);  // 격리 수준 설정
        definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);  // 전파 행동 설정

        // 트랜잭션 시작
        TransactionStatus status = transactionManager.getTransaction(definition);
		......

 

 

트랜잭션 매니저는 통상적으로 4가지 속성을 정의합니다.

  • 전파 속성: 트랜잭션의 전파 방식을 설정
  • 격리 수준 : 트랜잭션 격리 수준 설정
  • 타임 아웃 : 트랜잭션이 만료되기 전에 유지될 최대 시간
  • 읽기 전용 : 트랜잭션이 읽기 전용 여부인지 설정 
public interface TransactionDefinition {
    int PROPAGATION_REQUIRED = 0;
    int PROPAGATION_SUPPORTS = 1;
    int PROPAGATION_MANDATORY = 2;
    int PROPAGATION_REQUIRES_NEW = 3;
    int PROPAGATION_NOT_SUPPORTED = 4;
    int PROPAGATION_NEVER = 5;
    int PROPAGATION_NESTED = 6;
    int ISOLATION_DEFAULT = -1;
    int ISOLATION_READ_UNCOMMITTED = 1;
    int ISOLATION_READ_COMMITTED = 2;
    int ISOLATION_REPEATABLE_READ = 4;
    int ISOLATION_SERIALIZABLE = 8;
    int TIMEOUT_DEFAULT = -1;

    default int getPropagationBehavior() {
        return 0;
    }

    default int getIsolationLevel() {
        return -1;
    }

    default int getTimeout() {
        return -1;
    }

    default boolean isReadOnly() {
        return false;
    }

    @Nullable
    default String getName() {
        return null;
    }

    static TransactionDefinition withDefaults() {
        return StaticTransactionDefinition.INSTANCE;
    }
}

 

트랜잭션의 종료

하나의 트랜잭션이 시작되면 Commit() 또는 rollback()이 호출될 때까지 트랜잭션이 실행되는 상태입니다.

이 말은 즉 Commit() 또는 rollback()을 진행하면 트랜잭션이 종료된다는 뜻입니다. 

@Service
public class MyService {

    @Autowired
    private PlatformTransactionManager transactionManager;

    public void performTransaction() {
        DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
        definition.setIsolationLevel(DefaultTransactionDefinition.ISOLATION_READ_COMMITTED);
        definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);

        TransactionStatus status = transactionManager.getTransaction(definition);

        try {
            // 여러 DB 작업 실행

            // 트랜잭션 커밋
            transactionManager.commit(status);  // 트랜잭션 커밋
        } catch (Exception e) {
            // 예외 발생 시 롤백
            transactionManager.rollback(status);  // 트랜잭션 롤백
            throw e;  // 예외를 다시 던짐
        }
    }
}

 

Commit과 rollback

트랜잭션에서 커밋(commit)과 롤백(rollback)은 아주 중요한 역할을 합니다. 이들은 데이터베이스의 ACID를 보장하는 핵심요소입니다. 

  • 커밋(commit) : 모든 데이터 변경을 영구적으로 데이터베이스에 반영
  • 롤백(rollback) : 트랜잭션 내에서 발생한 변경 사항을 모두 되돌려 데이터베이스 복원

이 두 가지 일은 모두 하나의 트랜잭션 단위 안에서 일어납니다.

즉 하나의 트랜잭션이 실행될 때 트랜잭션 내에서 오류가 발생하면 모두 롤백하며, 정상 작동했다면 모두 커밋합니다.

이것을 원자성 보장(Atomicity)라고 합니다.

 

📝 트랜잭션 전파 속성을 사용하는 이유

Spring이 제공하는 트랜잭션의 장점 중 하나는 여러 트랜잭션을 묶어서 커다란 하나의 트랜잭션 경계를 만들 수 있다는 점입니다.

작업을 하다보면 기존에 트랜잭션이 진행 중일 때 추가적인 트랜잭션을 진행해야 하는 경우가 있습니다. 이때 이미 트랜잭션이 진행 중일 때 추가 트랜잭션 진행을 어떻게 할지 결정하는 것이 전파 속성입니다. 

 

이제 트랜잭션 전파 속성이 왜 필요한지 감이 잡힐 겁니다. 그것은 바로 트랜잭션의 범위를 정해 commit과 rollback을 제어하는 것입니다.

 

물리 트랜잭션과 논리 트랜잭션

 

트랜잭션은 데이터베이스에서 제공하는 기술이므로 커넥션 객체를 통해 처리합니다. 그래서 1개의 트랜잭션을 사용한다는 것은 하나의 커넥션 객체를 사용한다는 것을 의미하며 실제 데이터베이스의 트랜잭션을 사용하는 점에서 물리 트랜잭션이라 합니다.

물리 트랜잭션은 실제 커넥션에 롤백/커밋을 호출하는 것이므로 해당 트랜잭션이 끝나는 것입니다.

 

스프링의 트랜잭션은 그래서 2가지로 나뉩니다. 

  • 물리 트랜잭션 : 실제 데이터베이스의 트랜잭션으로, 커넥션을 통해 커밋/롤백을 하는 단위입니다.
  • 논리 트랜잭션 : 스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위입니다.

또한 논리 트랜잭션도 2가지로 나뉩니다. 

  • 외부 트랜잭션 : 트랜잭션 전파에서 부모 트랜잭션에 해당합니다. 주로 비즈니스 로직의 시작점입니다.
  • 내부 트랜잭션 : 외부 트랜잭션 내에서 실행되는 자식 트랜잭션입니다. 

 

여기서 내부 트랜잭션은 자체적으로 커밋/롤백을 수행하지 않고, 외부 트랜잭션의 커밋 또는 롤백에 종속적입니다. 

위 그림은 스프링의 기본 트랜잭선 전파 속성입니다. 이 경우 개별 논리 트랜잭션이 존재하지만 1개의 물리 트랜잭션이 적용됩니다.

이 경우 둘 중 하나라도 논리 트랜잭션에 롤백이 일어나면 롤백 체크를 하여 물리 트랜잭션도 롤백하게 됩니다.

  • 모든 논리 트랜잭션이 커밋되어야 물리 트랜잭션이 커밋된다.
  • 하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션도 롤백됩니다. 

 

📝 스프링의 전파 속성들

스프링은 7가지 전파 속성이 존재합니다. 그러나 우리는 가장 많이 쓰는 
두 케이스(REQUIRED, REQUIRES_NEW)를 보겠습니다. 

 

 

전파 속성 (Propagation) 설명 트랜잭션 중첩 시 동작 방식
REQUIRED 트랜잭션이 이미 존재하면 그 트랜잭션에 참여하고, 트랜잭션이 존재하지 않으면 새로운 트랜잭션을 시작합니다. 이미 존재하는 트랜잭션이 있으면 해당 트랜잭션에 참여, 없으면 새 트랜잭션 시작.
REQUIRES_NEW 항상 새로운 트랜잭션을 시작합니다. 이미 진행 중인 트랜잭션이 있으면, 그것을 일시 중단하고 새로운 트랜잭션을 시작합니다. 트랜잭션이 있을 경우 중단하고 새로운 트랜잭션 시작.
SUPPORTS 트랜잭션이 존재하면 참여하고, 없으면 트랜잭션 없이 실행됩니다. 트랜잭션이 있으면 참여하고, 없으면 트랜잭션 없이 실행.
NOT_SUPPORTED 트랜잭션을 사용하지 않고 실행됩니다. 만약 트랜잭션이 이미 존재한다면, 그것을 일시 중단하고 트랜잭션 없이 실행됩니다. 트랜잭션 없이 실행되며, 기존 트랜잭션이 있다면 중단.
MANDATORY 반드시 트랜잭션이 존재해야 하며, 없으면 예외를 발생시킵니다. 반드시 트랜잭션이 존재해야 하고, 없으면 예외 발생.
NEVER 트랜잭션 없이 실행되어야 하며, 만약 트랜잭션이 존재하면 예외를 발생시킵니다. 트랜잭션이 이미 존재하면 예외 발생.
NESTED 트랜잭션이 이미 존재하면 새로운 트랜잭션을 중첩(혹은 내포)하여 실행합니다. 존재하지 않으면 REQUIRED와 동일하게 동작하여 새로운 트랜잭션을 시작합니다. 트랜잭션 중첩을 허용, 이미 존재하는 트랜잭션 내에서 독립적인 트랜잭션을 시작하고, 독립적으로 롤백 가능.

 

여기서 트랜잭션에 참여한다는 것은 물리 트랜잭션을 그대로 이어받아 사용하는 것입니다.

 

public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
}

 

REQUIRED

 

REQUIRED는 스프링이 제공하는 기본 타입 전파 속성입니다. 기본적으로 다수의 트랜잭션을 묶어 1개의 물리 트랜잭션을 사용하는 것입니다. 

 

위에서 말했다싶이 해당 전파 속성은 논리 트랜잭션들 중 1개라도 롤백되었다면 물리 트랜잭션도 롤백됩니다. 

그러나 외부 트랜잭션에서 내부 트랜잭션으로 논리 트랜잭션을 연장한 것이기 때문에 롤백 시점에 따라 동작의 차이가 존재합니다.

 

  • 외부 트랜잭션에서 롤백
    • 외부 트랜잭션에서 롤백이 필요한 경우 내부 트랜잭션과 무관하게 물리 트랜잭션에서 롤백을 시킵니다.
  • 내부 트랜잭션에서 롤백
    • 내부 트랜잭션에서 롤백이 필요한 경우 내부 트랜잭션은 커밋/롤백이 외부 트랜잭션에 종속적이기에 외부 트랜잭션에 롤백의 필요성을 예외(UnexpectedRollbackException)로 알려 롤백을 처리합니다.

 

REQUIRES_NEW

 

REQUIRES_NEW는 외부 트랜잭션과 내부 트랜잭션을 완전히 분리하는 전파 속성입니다. 그래서 여러 개의 물리 트랜잭션이 사용됩니다. 각각 트랜잭션 별로 커밋과 롤백이 수행됩니다. 

 

두 개가 서로 다른 물리 트랜잭션이기에, 내부 트랜잭션 롤백이 외부 트랜잭션 롤백에 영향을 주지 않습니다. 

그러므로 내부 트랜잭션이 롤백 호출은 실제 커넥션에 롤백을 호출한 것이므로 트랜잭션이 끝나게 됩니다.

 

이것은 서로 다른 데이터베이스 커넥션을 적용한다는 것으로 1개의 요청에 대해 2개의 커넥션을 사용하는 것이므로 데이터베이스 커넥션 풀에 대한 낭비를 일으킬 수 있으므로 조심히 사용해야 합니다.

 

해당 기능은 보통 트랜잭션을 분리해야하는 시나리오에 사용됩니다.

  • 어떤 작업은 실패해도 다른 작업은 반드시 완료해야하는 경우
  • 외부 트랜잭션 내에서 여러 개의 다른 트랜잭션을 동시에 실행해야 할 경우
  • 외부 API 호출, 다른 데이터베이스에 대한 작업 또는 비즈니스 로직에서 일부만 롤백될 필요가 있을 때