스프링 트랜잭션 전파 옵션 중 REQUIRED랑 REQUIRES_NEW까지는 실무에서도 많이 쓰이고, 왜 존재하는지도 납득이 간다. 근데 SUPPORT, NOT_SUPPORTED, MANDATORY, NEVER 같은 옵션들을 마주하면 좀 당황스럽다. '실무에서 쓸 일이 거의 없는데, 굳이 왜 이렇게까지 옵션을 세분화해둔 거지?' 하는 의구심이 들기 때문이다.
결론부터 말하면, 이 옵션들은 단순히 기능을 구현하기 위한 도구라기보다 "내 로직에 실수가 끼어들지 못하게 만드는 안전장치"이자 "이 코드는 이렇게 돌아가야만 해"라고 못 박아두는 선언에 가깝다. 왜 이런 '까칠한' 옵션들이 만들어졌는지, 실무적인 관점에서 가볍게 정리해 보려고 한다.
1. MANDATORY & NEVER: "실수하면 차라리 터뜨려라"
이 옵션들은 주로 데이터의 정합성이 극도로 중요할 때 사용하는 안전장치다.
- MANDATORY (트랜잭션 필수)
- 적용 상황: 금융 시스템의 '잔액 차감' 로직.
- 예시: 잔액 차감은 단독으로 발생하면 안 된다. 반드시 '상품 구매'나 '계좌 이체'라는 상위 트랜잭션 안에서만 수행되어야 한다. 만약 개발자가 실수로 잔액 차감 로직만 따로 호출하는 API를 만들었다면? MANDATORY는 트랜잭션이 없으면 예외(IllegalTransactionStateException)를 던져 실행을 막음으로써, 근거 없이 돈이 빠져나가는 상황을 원천 봉쇄한다.
- NEVER (트랜잭션 금지)
- 적용 상황: 대량의 외부 이미지 업로드 또는 리소스를 많이 먹는 파일 변환.
- 예시: 만약 실수로 DB 트랜잭션 안에서 100장의 이미지를 S3에 업로드하는 무거운 작업을 호출한다면? 업로드가 끝날 때까지 DB 커넥션이 불필요하게 오래 점유되는 최악의 상황이 발생할 수 있다. NEVER를 걸어두면 트랜잭션 안에서 이 작업이 호출되는 순간 바로 예외를 발생시켜, DB 커넥션이 장시간 묶이는 일을 사전에 방지한다.
2. NOT_SUPPORTED: "내 작업 때문에 메인 DB를 멈출 순 없지"
트랜잭션이 아예 없는 게 아니라, 기존 트랜잭션을 잠시 멈추는(Suspend) 것이 핵심이다.
- 적용 상황: 주문 완료 직후 보여주는 비실시간 통계 조회.
- 예시: 주문은 0.1초 만에 끝나야 하지만, 그 밑에 붙는 "이 상품을 구매한 연령대 통계" 쿼리는 3초가 걸린다. 주문 트랜잭션 안에 이 3초짜리 조회를 묶어두면 그동안 트랜잭션이 불필요하게 길어지고 DB 커넥션 점유 시간이 증가할 수 있다. 이때 통계 로직에 NOT_SUPPORTED를 걸면, 메인 트랜잭션은 잠시 숨을 고르고(커넥션 점유 최소화), 통계는 트랜잭션 없이 가볍게 실행된다.
3. SUPPORTS: "있으면 따라가고, 없으면 혼자 가기"
단순히 @Transactional을 생략했을 때와 가장 큰 차이점은 '트랜잭션 컨텍스트를 공유하느냐'에 있다.(JPA의 경우 영속성 컨텍스트, MyBatis나 JdbcTemplate의 경우 동일한 DB 커넥션)
- 적용 상황: 여러 서비스에서 공통으로 쓰는 '상품 정보 조회' 모듈.
- 예시: 단순 상품 상세 페이지에서는 상품 정보를 트랜잭션 없이 빠르게 읽어야 하지만, '주문 로직' 중간에 상품 정보를 조회할 때는 방금 전 주문 로직이 수정한 상품 재고 상태를 반영한 동일한 트랜잭션 컨텍스트(JPA의 경우 영속성 컨텍스트)를 읽어와야 할 때가 있다. SUPPORTS를 쓰면 상위 트랜잭션이 있을 땐 그 흐름에 합류해 동일한 트랜잭션 컨텍스트를 공유하고, 없을 땐 트랜잭션 없이 가볍게 동작한다.
4. NESTED: "부분 손절은 가능, 하지만 운명 공동체"
REQUIRES_NEW와 비슷해 보이지만, 부모 트랜잭션의 운명에 종속된다는 점이 결정적이다.
- 적용 상황: 주문 프로세스 중의 '이벤트 알림톡 발송'.
- 예시: 주문은 성공해야 하지만, 알림톡 발송 실패 때문에 주문 자체가 취소(Rollback)되는 건 원치 않는다. 그렇다고 알림톡 발송만 성공하고 주문이 실패하는 것도 안 된다. 이 경우, NESTED를 사용하면 예외를 내부에서 적절히 처리한다는 전제 하에, 알림톡 발송이 실패해도 알림톡 로직만 롤백하고 주문은 성공시킬 수 있다. 내부적으로 세이브포인트(savepoint)를 찍고 동작하기 때문인데, 덕분에 알림톡 로직이 시작된 지점(savepoint)까지만 되돌릴 수 있다. 하지만 주문 자체가 실패하면 (savepoint 위에서 동작하기 때문에) 알림톡 기록도 함께 롤백되어 데이터의 일관성을 맞춘다.
💡마치며
결국 이 전파 옵션들은 "이 비즈니스 로직을 DB 커넥션과 트랜잭션이라는 울타리 안에 어디까지 가둘 것인가?"를 고민하는 도구인 것 같다. 사실 실무에서 이 옵션들을 쓸 일은 거의 없지만, 적당히 선을 긋는 감각은 확실히 필요해 보인다.
예를 들어, 데이터의 정합성이 목숨보다 중요한 금융 로직에서는 MANDATORY나 NEVER로 아예 실수를 못 하게 막아버리는 식이다. 반대로 성능과 효율이 우선인 대규모 조회나 통계 작업에서는 NOT_SUPPORTED나 SUPPORTS를 써서 DB 커넥션이 불필요하게 묶이지 않게 교통정리를 해줄 수도 있다. 또, 알림 발송처럼 '주문은 성공해도 이건 실패할 수 있는' 상황에선 NESTED로 살짝 보험을 들어두는 것도 방법이다.
단순히 기능을 구현하는 것을 넘어, 내가 짠 코드가 DB 자원을 얼마나 점유하고 다른 로직에 어떤 영향을 줄지 한 번쯤 고민해 본 것만으로도 공부한 보람은 있는 것 같다. 당장 실무에서 다 쓰진 않더라도, 나중에 비슷한 고민이 생길 때 "아, 그때 그 옵션!"하고 떠올릴 수만 있어도 충분하지 않을까.