TIL

Spring 트랜잭션 전파 레벨

하얀잔디 2025. 5. 22. 16:42

예약 저장을 한다고 하자.

  • 예약 정보 DB 저장
  • 알림 테이블에 insert
  • 히스토리 로그 저장

이 3가지가 동시에 일어나야 했다. 문제는, 알림 로직이 다른 서비스에서도 공통으로 쓰이다 보니

@Transactional(propagation = REQUIRES_NEW)로 설정돼 있었던 것. ( 새로운 트랜잭션. 다른 트랜잭션 중단)


문제 발생

  1. 로그 저장 중 예외가 발생해서 전체 트랜잭션이 롤백되었는데
  2. 알림은 DB에 남아 있었다...

알고 보니, 알림 로직에 설정된 REQUIRES_NEW 때문에 별도 트랜잭션으로 commit 되어버린 것


Spring Propagation 기본


REQUIRED (기본값) 기존 트랜잭션이 있으면 참여, 없으면 새로 만듦 보통 대부분 이걸 씀 
REQUIRES_NEW ** 체크** 항상 새 트랜잭션을 만듦 (기존 트랜잭션 일시 중단) 알림, 로그 등 실패해도 별도로 남기고 싶은 경우
NESTED 트랜잭션 중첩. savepoint 활용 주로 rollback 시 일부만 롤백하고 싶은 경우
SUPPORTS 트랜잭션이 있으면 참여, 없으면 그냥 비트랜잭션 조회 전용에서 가볍게
NOT_SUPPORTED 트랜잭션 없이 실행 트랜잭션이 오히려 방해될 때
NEVER 트랜잭션 있으면 예외 발생 거의 안 씀
MANDATORY 반드시 트랜잭션 있어야 함 보통 내부 로직 보호용

 

 

 

 깨달음

  • REQUIRES_NEW는 별도 트랜잭션이라 롤백되지 않는다.
  • 진짜 전체 작업이 실패했을 때 다 같이 롤백되길 원하면, 하위 서비스들도 REQUIRED여야 한다.
  • 대신 로그 저장, 메일 전송처럼 "실패해도 되지만 저장은 하고 싶다"면 REQUIRES_NEW가 맞다.

 

변경된 방식

 
// 예약 저장 use case

@Transactional
fun saveReservation(req: ReservationRequest)
{

reservationRepository.save(req.toEntity())

try { // 알림도 같은 트랜잭션에서 처리
   alarmService.sendReservationAlarm(req.userId)
}
catch (e: Exception)
{
      log.warn("알림 전송 실패: ${e.message}") // 실패해도 전체 롤백 X, 무시
}

historyService.saveReservationHistory(req)
}

→ alarmService는 REQUIRES_NEW 제거하고, 대신 try-catch로 묶어서 전체 롤백에 영향 주지 않도록 조정하기.


정리

  • 의도한 대로 commit/rollback 시점을 맞추고 싶으면 propagation을 명확히 지정해야 한다.
  • 특히 공통 모듈로 만든 알림, 로그, 통계 서비스는 대부분 REQUIRES_NEW 걸려 있을 가능성이 크니, 트랜잭션 격리 여부 꼭 확인하자.

rollbackFor, noRollbackFor, readOnly 옵션 관련은 다음에~