1. TransactionTemplate

1.1 공식 정의

트랜잭션 경계 제어를 코드로 명시하기 위한 수단

  • 프로그래밍 방식 트랜잭션 관리 도구
  • 트랜잭션 시작 / 커밋 / 롤백을 템플릿이 대신 처리
  • 개발자는 “트랜잭션 안에서 실행할 로직”만 작성

2. 왜 TransactionTemplate가 필요할까

2.1 @Transactional의 한계

Spring에서 가장 흔히 사용하는 트랜잭션 방식은 @Transactional이다.

@Transactional
public void save() {
    // business logic
}

하지만 이 방식에는 명확한 한계가 있다.

  • 메서드 단위로만 트랜잭션 경계 설정 가능
  • 반복문 내부에서 트랜잭션 분리 ❌
  • 조건에 따라 트랜잭션 적용 여부를 바꾸기 어려움
  • 멀티스레드 환경에서는 적용 불가

즉, @Transactional선언적 트랜잭션에는 편하지만, 세밀한 제어에는 부적합하다.


2.2 프로그래밍 방식 트랜잭션의 필요성

다음과 같은 상황에서는 선언적 트랜잭션이 부족해진다.

  • 반복 작업마다 트랜잭션을 분리해야 할 때
  • 멀티스레드 환경에서 명시적으로 트랜잭션을 열어야 할 때
  • 트랜잭션 경계를 코드 흐름에 따라 결정해야 할 때
  • 배치 / 테스트 데이터 초기화 코드

이때 TransactionTemplate을 사용한다.


2.3 @Transactional을 사용하면 어떻게 될까?

앞선 설명을 보고 나면 자연스럽게 다음 질문이 생긴다.

“그냥 @Transactional을 쓰면

대량 데이터도 하나의 트랜잭션으로 처리할 수 있는 것 아닌가?”

결론부터 말하면 가능은 하지만, 이 예제에서는 적절하지 않다.


2.3.1 하나의 트랜잭션으로 묶였을 때의 문제

만약 아래와 같이 @Transactional을 사용한다면,

@Transactional
public void insertAll() {
    for (int i = 0; i < 12_000_000; i++) {
        entityManager.persist(article);
    }
}

1,200만 건의 insert가 하나의 트랜잭션으로 묶이게 된다.
이 경우 트랜잭션은 다음과 같은 문제를 갖는다.

  • 트랜잭션이 지나치게 커진다
  • 롤백 시 undo log 비용이 감당 불가능한 수준이 된다
  • 락을 장시간 점유해 다른 트랜잭션을 막는다
  • 장애 발생 시 모든 작업이 한 번에 롤백된다

2.3.2 멀티스레드 환경에서는 더 위험해진다

지금까지는 단일 스레드에서 하나의 트랜잭션이 너무 커지는 문제를 살펴봤다.
하지만 실제 대량 데이터 처리나 배치 작업에서는
처리 속도를 위해 멀티스레드 병렬 실행을 함께 사용하는 경우가 많다.

이때 @Transactional의 한계는 더욱 명확해진다.

ExecutorService executorService = Executors.newFixedThreadPool(10);
 
executorService.submit(() -> insert());

위 코드에서 중요한 점은 다음이다.

  • insert()메인 스레드가 아닌
  • ExecutorService가 관리하는 워커 스레드에서 실행된다

즉, 이 시점부터 트랜잭션은 **“어느 스레드에서 시작되었는가”**가 핵심 문제가 된다.

하지만 @Transactional은 다음과 같은 특성을 가진다.

  • 트랜잭션은 ThreadLocal 기반으로 관리된다
  • 메서드를 호출한 현재 스레드에만 적용된다
  • 다른 스레드로 자동 전파되지 않는다

이 구조를 그대로 적용하면 다음과 같은 문제가 발생한다.

  • 메인 스레드에서 시작한 @Transactional은 워커 스레드에서 실행되는 insert()에 적용되지 않는다
  • 결과적으로 워커 스레드의 persist() 호출은 트랜잭션 없이 실행될 수 있다
  • 트랜잭션 경계가 코드 상으로도, 실행 시점으로도 불분명해진다

즉, 멀티스레드 환경에서 @Transactional을 사용하면

  • 트랜잭션이 적용되지 않거나
  • 예상과 다른 시점에 커밋·롤백되거나
  • 트랜잭션 경계가 개발자의 의도와 어긋난다

따라서 @Transactional

병렬 배치 작업을 전제로 설계된 도구가 아니다.


2.3.3 결과적으로 트랜잭션의 의도가 훼손된다

이런 상황에서 @Transactional은 다음 중 하나가 된다.

  • 너무 커서 롤백할 수 없는 트랜잭션
  • 장애 시 모든 작업을 무효화하는 트랜잭션
  • 멀티스레드 환경에서 적용되지 않는 트랜잭션

3. TransactionTemplate의 역할

TransactionTemplate은 다음을 책임진다.

  1. 트랜잭션 시작 (BEGIN)
  2. 콜백 코드 실행
  3. 정상 종료 시 → COMMIT
  4. 예외 발생 시 → ROLLBACK

개발자는 트랜잭션 제어 코드를 직접 작성하지 않는다.


4. TransactionTemplate의 사용 방식

4.1 기본 사용 예시

transactionTemplate.executeWithoutResult(status -> {
    // 트랜잭션 내부 로직
});

이 코드의 의미는 다음과 같다.

  • 람다 시작 시점 → 트랜잭션 시작
  • 람다 정상 종료 → 커밋
  • 예외 발생 → 롤백

람다 블록 자체가 하나의 트랜잭션 경계


4.2 execute vs executeWithoutResult

메서드설명
execute()결과를 반환하는 트랜잭션
executeWithoutResult()결과 없이 실행

5. 예시

아래 코드는 대량 데이터를 병렬로 삽입하는 테스트 코드의 일부다.

@Autowired
TransactionTemplate transactionTemplate;
 
void insert() {
    transactionTemplate.executeWithoutResult(status -> {
        // 이 블록 진입 시점에 트랜잭션 시작
        for (int idx = 0; idx < BULK_INSERT_SIZE; idx++) {
            Article article = Article.create(...);
            entityManager.persist(article);
        }
        // 이 블록 정상 종료 시점에 flush + commit
    });
}

이 코드에서 TransactionTemplate의 역할은 단순히 “트랜잭션을 여는 것”에 그치지 않는다.

  • Spring이 관리하는 PlatformTransactionManager를 기반으로 트랜잭션의 시작·커밋·롤백을 코드 블록 단위로 제어한다
  • 선언적 트랜잭션(@Transactional)로는 불가능한 세밀한 트랜잭션 경계 제어를 가능하게 한다
  • 멀티스레드 환경에서도 각 작업을 독립적인 트랜잭션으로 분리한다

즉, 이 코드는 배치 작업과 병렬 실행을 전제로 한 트랜잭션 설계다.


5.1 트랜잭션 단위 제어

void insert() {
    transactionTemplate.executeWithoutResult(status -> {
        for (int idx = 0; idx < BULK_INSERT_SIZE; idx++) {
            entityManager.persist(article);
        }
    });
}

이 코드의 트랜잭션 단위는 다음과 같이 정의된다.

  • insert() 호출 1회 = 트랜잭션 1개
  • 트랜잭션마다 독립적인 영속성 컨텍스트 생성
  • 코드 블록이 정상 종료되는 시점에 flush → commit 순서로 트랜잭션이 완료된다

중요한 점은 트랜잭션의 경계가 메서드 선언이 아니라 코드 블록에 명시되어 있다는 것이다.

즉, TransactionTemplate

트랜잭션 단위를 메서드가 아닌 코드 흐름 기준으로 제어하게 해준다.

이 특성 덕분에 반복문, 조건문, 배치 로직과 자연스럽게 결합할 수 있다.


5.2 멀티스레드 환경에서의 의미

executorService.submit(() -> insert());

이 코드와 TransactionTemplate이 결합되면 멀티스레드 환경에서도 트랜잭션 경계가 명확해진다.

이 구조에서의 동작은 다음과 같다.

  • 각 스레드는 insert()를 독립적으로 호출
  • 호출될 때마다 TransactionTemplate새 트랜잭션을 생성
  • 생성된 트랜잭션은 해당 스레드에 바인딩
  • 스레드 간 트랜잭션 공유는 발생하지 않는다

그 결과:

  • 한 스레드의 실패가 다른 스레드의 트랜잭션에 영향을 주지 않고
  • 각 작업은 독립적으로 커밋 또는 롤백된다

따라서 이 구조는 병렬 배치 작업에서 요구되는 트랜잭션 안정성을 만족한다.


5.3 정리

이 예제에서 TransactionTemplate의 핵심 역할은 다음과 같다.

  • 트랜잭션 경계를 코드 블록 단위로 명확히 표현
  • 반복·배치·병렬 작업에 적합한 트랜잭션 구조 제공
  • 멀티스레드 환경에서도 안전한 트랜잭션 분리 보장

이 때문에 해당 코드에서는 @Transactional이 아니라 TransactionTemplate이 선택되었다.


6. TransactionTemplate를 사용하는 경우

6.1 적절한 경우

  • 배치 / 대량 insert 작업
  • 반복문 내부에서 트랜잭션을 분리해야 할 때
  • 멀티스레드 환경
  • 테스트 데이터 초기화
  • 트랜잭션 경계를 명시적으로 제어해야 할 때

6.2 피해야 하는 경우

  • 일반적인 서비스 로직
  • 단순 CRUD
  • 트랜잭션 경계가 메서드 단위로 충분한 경우

이 경우에는 @Transactional이 더 간결하고 읽기 쉽다.


7. @Transactional과 TransactionTemplate 비교

구분@TransactionalTransactionTemplate
방식선언적프로그래밍
트랜잭션 경계메서드 단위코드 블록 단위
반복문 제어어려움가능
멀티스레드부적합적합
가독성높음상대적으로 낮음

8. 요약

TransactionTemplate은 선언적 트랜잭션이 감당하지 못하는 영역에서,

트랜잭션 경계를 코드로 직접 제어하기 위해 제공되는 Spring의 도구다.


참고 자료 (출처)