Spring @Transactional - REQUIRES_NEW vs REQUIRED, 그리고 실전 적용 사례
1. 개요
Spring에서는 @Transactional을 활용하여 트랜잭션을 관리할 수 있다. 하지만 단순히 @Transactional을 선언하는 것만으로 충분하지 않을 때가 많다. 특히 트랜잭션 전파(Propagation) 옵션을 적절히 선택하지 않으면 예상치 못한 문제가 발생할 수 있다.
이번 글에서는 REQUIRES_NEW와 REQUIRED의 차이점을 실무에서 발생했던 이슈와 함께 설명하고, 커스텀 예외 처리 적용 사례를 통해 이를 어떻게 해결할 수 있는지 정리해보려고 한다.
2. @Transactional의 전파(Propagation) 개념
1) REQUIRED (기본값)
- 부모 트랜잭션이 있으면 해당 트랜잭션을 공유하여 실행된다.
- 부모 트랜잭션이 없으면 새로운 트랜잭션이 생성된다.
- 부모 트랜잭션이 롤백되면, 내부에서 실행된 트랜잭션도 롤백된다.
@Service
public class OrderService {
@Transactional // propagation 기본값은 REQUIRED
public void placeOrder() {
processOrder();
paymentService.processPayment(); // 같은 트랜잭션 내에서 실행됨
}
}
2) REQUIRES_NEW (새로운 트랜잭션 강제 생성)
- 항상 새로운 트랜잭션을 생성하여 실행된다.
- 부모 트랜잭션이 있으면 부모 트랜잭션을 일시 중단(Suspend)하고 새로운 트랜잭션을 시작한다.
- 부모 트랜잭션이 롤백되더라도, REQUIRES_NEW에서 실행된 트랜잭션은 롤백되지 않는다.
@Service
public class NotificationService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotifications(Set<Long> userNoSet) {
if (!userNoSet.isEmpty()) {
notificationRepository.saveAll(userNoSet);
}
}
}
3. 실무에서 겪은 문제 - REQUIRES_NEW의 필요성
문제 상황
처음에는 모든 서비스 메서드를 기본값(REQUIRED)으로 설정하여 사용하고 있었다. 그런데 다음과 같은 문제가 발생했다.
- 주문 처리 중 알림 전송 오류 발생 시, 주문도 롤백됨
- placeOrder() 메서드에서 주문을 저장한 후, sendNotifications()를 호출하여 알림을 전송했다.
- 그런데 알림 전송 중 예외 발생 시, 주문까지 롤백되는 문제가 발생했다.
- 원인은 sendNotifications()가 부모 트랜잭션(주문 처리 트랜잭션)과 같은 트랜잭션을 공유했기 때문이었다.
@Service
public class OrderService {
@Transactional
public void placeOrder() {
orderRepository.save(order); // 주문 저장
// 알림 전송 중 예외 발생 -> 주문까지 롤백되는 문제 발생
notificationService.sendNotifications(userNoSet);
}
}
해결 방법 - REQUIRES_NEW 적용
주문 처리 트랜잭션과 알림 전송 트랜잭션을 분리하기 위해, sendNotifications()에 REQUIRES_NEW를 적용했다.
@Service
public class NotificationService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotifications(Set<Long> userNoSet) {
notificationRepository.saveAll(userNoSet);
}
}
이렇게 변경한 후, 알림 전송 중 예외가 발생하더라도 주문 처리는 롤백되지 않고 정상적으로 커밋되었다.
4. 커스텀 예외 처리 적용 - 비즈니스 로직의 정합성 유지
REQUIRES_NEW를 적용한 후, 알림 전송이 실패해도 주문 처리가 유지되었다. 하지만 알림 전송이 실패했다는 정보를 남기고, 이를 나중에 재처리할 방법이 필요했다.
이를 위해 커스텀 예외 처리 및 롤백 정책을 추가했다.
1) 커스텀 예외 생성
public class NotificationException extends RuntimeException {
public NotificationException(String message) {
super(message);
}
}
2) 예외 발생 시 알림 재처리 로직 추가
@Service
public class NotificationService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendNotifications(Set<Long> userNoSet) {
try {
notificationRepository.saveAll(userNoSet);
} catch (Exception e) {
// 알림 전송 실패 로그 저장 후 예외 발생
log.error("알림 전송 실패: {}", e.getMessage());
throw new NotificationException("알림 전송 중 오류 발생");
}
}
}
3) 주문 서비스에서 예외 캐치 및 재처리 큐 등록
@Service
public class OrderService {
@Transactional
public void placeOrder() {
orderRepository.save(order);
try {
notificationService.sendNotifications(userNoSet);
} catch (NotificationException e) {
log.warn("알림 전송 실패, 재처리 큐에 추가");
notificationRetryQueue.add(userNoSet); // 실패한 알림을 재처리 큐에 등록
}
}
}
5. 정리 - REQUIRES_NEW가 필요한 순간
✅ 언제 REQUIRES_NEW를 사용해야 할까?
- 부모 트랜잭션과 독립적으로 실행해야 할 때
- 예: 로그 저장, 알림 전송, 이메일 발송, 감사 로그 기록
- 부모 트랜잭션이 롤백되더라도 특정 작업을 유지해야 할 때
- 예: 주문은 롤백되지 않고, 결제만 롤백해야 할 때
- 외부 API 호출 또는 별도 비즈니스 프로세스를 처리할 때
- 예: 결제 요청을 외부 PG사에 보낼 때, 원본 트랜잭션과 별도로 실행해야 할 경우
❌ 언제 REQUIRES_NEW를 피해야 할까?
- 트랜잭션이 자주 생성되면 성능 문제가 발생할 가능성이 있음
- 불필요하게 REQUIRES_NEW를 사용하면 트랜잭션 오버헤드가 커질 수 있음.
- DB 락(Lock)으로 인해 데드락이 발생할 수 있음
- 부모 트랜잭션과 새로운 트랜잭션이 서로 같은 데이터를 갱신하는 경우 데드락 가능성이 있음.
6. 마무리
Spring의 트랜잭션 전파(Propagation) 옵션은 단순 개념 정리를 넘어서, 실제 비즈니스 로직에서 정확한 트랜잭션 흐름을 설계하는 것이 중요하다.
특히 REQUIRES_NEW를 적절히 활용하면 트랜잭션을 독립적으로 관리할 수 있어 예기치 않은 롤백 문제를 방지할 수 있다. 하지만 남용하면 성능 저하나 데드락 문제가 발생할 수 있으므로, 실제 서비스 아키텍처를 고려하여 신중하게 적용해야 한다.
'백엔드 프레임워크 > SpringBoot' 카테고리의 다른 글
[Spring Boot] 실무에서의 디자인 패턴과 도메인 디자인 패턴 적용 사례 (0) | 2025.03.11 |
---|