예약 저장을 한다고 하자.
- 예약 정보 DB 저장
- 알림 테이블에 insert
- 히스토리 로그 저장
이 3가지가 동시에 일어나야 했다. 문제는, 알림 로직이 다른 서비스에서도 공통으로 쓰이다 보니
@Transactional(propagation = REQUIRES_NEW)로 설정돼 있었던 것. ( 새로운 트랜잭션. 다른 트랜잭션 중단)
문제 발생
- 로그 저장 중 예외가 발생해서 전체 트랜잭션이 롤백되었는데
- 알림은 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 옵션 관련은 다음에~
'TIL' 카테고리의 다른 글
Kafka 동시에 같은 이벤트 들어오는 경우 (0) | 2025.05.25 |
---|---|
Spring 트랜잭션 RollbackFor (0) | 2025.05.22 |
Flutter 앱 아이콘 / 스플래시 이미지 적용법 (0) | 2025.05.21 |
UNDO 공간부족 오류란 (0) | 2025.05.20 |
DB 마이그레이션 적용하는 법 (1) | 2025.05.20 |