TransactionTemplate — 선언적 트랜잭션이 맞지 않을 때 프로그래밍 방식 사용법
@Transactional만으로 해결되지 않는 트랜잭션 경계가 있다면, 코드로 직접 트랜잭션을 제어해야 하는 순간이 오지 않을까요?
솔직히 처음에는 "Spring에서 트랜잭션은 @Transactional 붙이면 끝 아닌가?"라고 생각했습니다. 하지만 실무에서 배치 처리나 외부 API 연동을 하다 보면 어노테이션 하나로는 원하는 트랜잭션 경계를 만들 수 없는 상황을 만나게 됩니다.
왜 프로그래밍 방식이 필요한가
@Transactional은 메서드 단위 로 트랜잭션 경계를 설정합니다. 하지만 다음과 같은 경우에는 맞지 않습니다.
- 하나의 메서드 안에서 여러 독립 트랜잭션이 필요한 경우
- ** 루프 안에서 건별로 커밋/롤백을 분리해야 하는 경우**
- ** 조건에 따라 트랜잭션 범위를 동적으로 결정해야 하는 경우**
- ** 같은 클래스 내부 호출이라 프록시를 우회하는 경우**
// 이런 요구사항은 @Transactional로 해결하기 어렵다
public void processBatch(List<Order> orders) {
for (Order order : orders) {
// 건별로 독립 트랜잭션 — 하나가 실패해도 나머지는 커밋되어야 함
try {
processOrder(order); // @Transactional을 붙여도 self-invocation 문제
} catch (Exception e) {
log.error("주문 처리 실패: {}", order.getId(), e);
}
}
}
이럴 때 TransactionTemplate이나 PlatformTransactionManager를 직접 사용합니다.
TransactionTemplate 기본 사용법
TransactionTemplate은 Spring이 제공하는 프로그래밍 방식 트랜잭션의 핵심 클래스입니다. 템플릿-콜백 패턴으로, 트랜잭션 시작/커밋/롤백을 TransactionTemplate이 담당하고 우리는 ** 비즈니스 로직에만 집중 **하면 됩니다.
설정
@Configuration
public class TransactionConfig {
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager txManager) {
return new TransactionTemplate(txManager);
}
}
또는 필요한 곳에서 직접 생성해도 됩니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final PlatformTransactionManager txManager;
// 필드에서 직접 생성
private TransactionTemplate txTemplate;
@PostConstruct
void init() {
this.txTemplate = new TransactionTemplate(txManager);
}
}
반환값이 있는 경우 — TransactionCallback
@Service
@RequiredArgsConstructor
public class OrderService {
private final TransactionTemplate txTemplate;
private final OrderRepository orderRepository;
public OrderDto createOrder(OrderCreateRequest request) {
// execute()의 반환값이 그대로 전달됨
return txTemplate.execute(status -> {
Order order = Order.create(request);
orderRepository.save(order);
return OrderDto.from(order);
// 정상 종료 → 자동 커밋
// 예외 발생 → 자동 롤백
});
}
}
execute() 메서드에 전달하는 람다가 TransactionCallback<T>입니다. 콜백이 정상 종료되면 커밋, 언체크 예외가 발생하면 롤백합니다.
반환값이 없는 경우 — TransactionCallbackWithoutResult
public void cancelOrder(Long orderId) {
txTemplate.executeWithoutResult(status -> {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.cancel();
orderRepository.save(order);
});
}
executeWithoutResult()는 Spring 5.2에서 추가된 편의 메서드입니다. 내부적으로 TransactionCallbackWithoutResult를 사용합니다.
이전 방식도 여전히 동작합니다.
// 이전 방식 (익명 클래스)
txTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
// 비즈니스 로직
}
});
롤백 제어 — setRollbackOnly()
예외를 던지지 않고도 롤백할 수 있습니다. TransactionStatus.setRollbackOnly()를 호출하면 콜백이 정상 종료되더라도 ** 커밋 대신 롤백이 실행됩니다 **.
public OrderResult processOrder(Order order) {
return txTemplate.execute(status -> {
orderRepository.save(order);
// 재고 확인
boolean hasStock = inventoryService.checkStock(order.getProductId(), order.getQuantity());
if (!hasStock) {
status.setRollbackOnly(); // 예외 없이 롤백 마킹
return OrderResult.outOfStock();
}
inventoryService.deductStock(order.getProductId(), order.getQuantity());
return OrderResult.success();
});
}
이 패턴은 비즈니스 로직 상 롤백이 필요하지만 예외를 던지고 싶지 않을 때 유용합니다. 호출자에게 예외 대신 결과 객체를 반환할 수 있습니다.
PlatformTransactionManager 직접 사용
더 세밀한 제어가 필요하면 PlatformTransactionManager를 직접 사용할 수 있습니다. TransactionTemplate보다 저수준이지만, try-catch-finally를 직접 작성해야 합니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final PlatformTransactionManager txManager;
private final OrderRepository orderRepository;
public void processWithManualControl(Order order) {
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
def.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
def.setTimeout(10); // 초 단위
TransactionStatus status = txManager.getTransaction(def);
try {
orderRepository.save(order);
// ... 비즈니스 로직 ...
txManager.commit(status);
} catch (Exception e) {
txManager.rollback(status);
throw e;
}
}
}
이 방식은 ** 트랜잭션 속성을 런타임에 동적으로 설정 **할 수 있다는 장점이 있습니다. 하지만 커밋/롤백을 빼먹으면 커넥션 누수가 발생하므로 주의해야 합니다.
대부분의 경우 TransactionTemplate으로 충분합니다. PlatformTransactionManager 직접 사용은 프레임워크나 라이브러리 수준의 코드에서나 필요합니다.
실무 시나리오
1. 배치 처리에서 건별 트랜잭션
가장 흔한 사용 사례입니다. 1000건의 데이터를 처리할 때, 한 건의 실패가 나머지 999건의 롤백으로 이어지면 안 됩니다.
@Service
@RequiredArgsConstructor
public class OrderBatchService {
private final TransactionTemplate txTemplate;
private final OrderRepository orderRepository;
private final PaymentService paymentService;
public BatchResult processBatch(List<OrderRequest> requests) {
int success = 0;
int fail = 0;
for (OrderRequest request : requests) {
try {
// 건별로 독립적인 트랜잭션
txTemplate.executeWithoutResult(status -> {
Order order = Order.create(request);
orderRepository.save(order);
paymentService.pay(order);
});
success++;
} catch (Exception e) {
// 이 건만 롤백됨, 다른 건에 영향 없음
log.error("처리 실패: {}", request.getOrderNo(), e);
fail++;
}
}
return new BatchResult(success, fail);
}
}
2. 외부 API 호출 후 부분 커밋
외부 API 호출 결과에 따라 DB 트랜잭션을 커밋할지 결정해야 하는 상황입니다.
@Service
@RequiredArgsConstructor
public class PaymentService {
private final TransactionTemplate txTemplate;
private final PaymentRepository paymentRepository;
private final ExternalPaymentGateway gateway;
public PaymentResult processPayment(PaymentRequest request) {
// 1단계: DB에 결제 요청 기록 (독립 트랜잭션)
Payment payment = txTemplate.execute(status -> {
Payment p = Payment.createPending(request);
return paymentRepository.save(p);
});
// 2단계: 외부 API 호출 (트랜잭션 밖에서)
GatewayResponse response;
try {
response = gateway.requestPayment(payment.toGatewayRequest());
} catch (Exception e) {
// 3-a: 외부 API 실패 — 상태 업데이트
txTemplate.executeWithoutResult(status -> {
payment.markFailed(e.getMessage());
paymentRepository.save(payment);
});
return PaymentResult.failed(e.getMessage());
}
// 3-b: 외부 API 성공 — 결제 확정
txTemplate.executeWithoutResult(status -> {
payment.markCompleted(response.getTransactionId());
paymentRepository.save(payment);
});
return PaymentResult.success(payment.getId());
}
}
이 예시에서 외부 API 호출은 ** 트랜잭션 밖에서** 실행됩니다. 만약 @Transactional 안에서 외부 API를 호출하면 API 응답이 느릴 때 DB 커넥션이 불필요하게 점유됩니다.
3. 트랜잭션 속성을 동적으로 결정
public void executeWithDynamicConfig(String operationType, Runnable task) {
TransactionTemplate dynamicTemplate = new TransactionTemplate(txManager);
// 운영 유형에 따라 타임아웃 조절
if ("heavy".equals(operationType)) {
dynamicTemplate.setTimeout(60);
dynamicTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
} else {
dynamicTemplate.setTimeout(10);
}
dynamicTemplate.executeWithoutResult(status -> task.run());
}
@Transactional vs TransactionTemplate 선택 기준
| 상황 | 추천 방식 |
|---|---|
| 일반 서비스 메서드의 CRUD | @Transactional |
| 메서드 전체가 하나의 트랜잭션 | @Transactional |
| 하나의 메서드에 여러 독립 트랜잭션 | TransactionTemplate |
| 배치에서 건별 트랜잭션 분리 | TransactionTemplate |
| 외부 API와 DB 작업을 분리 | TransactionTemplate |
| 조건에 따라 트랜잭션 범위 변경 | TransactionTemplate |
| 같은 클래스 내부 호출 | TransactionTemplate (프록시 우회) |
| 테스트에서 명시적 트랜잭션 제어 | TransactionTemplate |
원칙은 간단합니다. "메서드 = 트랜잭션"일 때는 @Transactional, ** 그 외에는 TransactionTemplate**입니다.
주의사항
TransactionTemplate은 스레드 안전하다
TransactionTemplate은 내부 상태가 불변(생성 후 설정을 변경하지 않는다면)이므로 ** 싱글톤 빈으로 등록하여 여러 스레드에서 동시에 사용 **할 수 있습니다.
@Bean
public TransactionTemplate transactionTemplate(PlatformTransactionManager txManager) {
TransactionTemplate template = new TransactionTemplate(txManager);
template.setTimeout(30); // 기본 타임아웃
return template; // 싱글톤으로 안전하게 재사용 가능
}
다만 설정이 다른 TransactionTemplate이 필요하면 별도 인스턴스를 만들어야 합니다.
@Bean
public TransactionTemplate readOnlyTxTemplate(PlatformTransactionManager txManager) {
TransactionTemplate template = new TransactionTemplate(txManager);
template.setReadOnly(true);
return template;
}
@Bean
public TransactionTemplate requiresNewTxTemplate(PlatformTransactionManager txManager) {
TransactionTemplate template = new TransactionTemplate(txManager);
template.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
return template;
}
체크 예외 처리에 주의
TransactionTemplate은 @Transactional과 마찬가지로 ** 언체크 예외에서만 자동 롤백 **합니다. 체크 예외는 콜백에서 던질 수 없기 때문에(람다 시그니처 제한) 직접 처리해야 합니다.
txTemplate.executeWithoutResult(status -> {
try {
riskyOperation(); // IOException 발생 가능
} catch (IOException e) {
status.setRollbackOnly(); // 명시적 롤백 마킹
throw new RuntimeException("처리 실패", e); // 언체크로 래핑
}
});
과도한 사용은 피하자
TransactionTemplate이 강력하다고 해서 모든 곳에 사용하면 코드가 복잡해집니다. 공부하다 보니 "이건 그냥 @Transactional로 충분한데 굳이 TransactionTemplate을 쓴다"는 코드 리뷰 피드백을 본 적이 있습니다.
// 불필요한 TransactionTemplate 사용 — @Transactional이 더 깔끔
public OrderDto getOrder(Long id) {
return txTemplate.execute(status -> {
return orderRepository.findById(id)
.map(OrderDto::from)
.orElseThrow();
});
}
// 이게 더 낫다
@Transactional(readOnly = true)
public OrderDto getOrder(Long id) {
return orderRepository.findById(id)
.map(OrderDto::from)
.orElseThrow();
}
정리
TransactionTemplate은@Transactional로 해결하기 어려운 ** 동적 트랜잭션 경계, 부분 커밋, 건별 트랜잭션 **에 사용합니다.execute()는 정상 종료 시 커밋, 예외 시 롤백을 ** 자동으로** 처리합니다.setRollbackOnly()로 예외 없이도 ** 명시적 롤백 **이 가능합니다.PlatformTransactionManager직접 사용은 가장 저수준이며, 대부분TransactionTemplate으로 충분합니다.- TransactionTemplate은 ** 스레드 안전하고 재사용 가능 **합니다.
- "메서드 = 트랜잭션"이면
@Transactional, 그 외에는TransactionTemplate이 선택 기준입니다.