요약
- 문제: 롤백 말고 그냥 흐르게 하고 싶은데 롤백이 된다.
- 원인: 스프링에 기본적으로 RuntimeExcepion이 발생하면 롤백을 시킨단다.
- 해결방법: 몇 가지 방법들이 있음 & 해결방법에 따라 결과가 다름. 주의!!
[해결방법]
- 트랜잭션 전파 전략=RequiresNew를 사용한다. 즉, 메서드를 별도 트랜잭션으로 처리한다
@Transactional(propagation = Propagation.REQUIRES_NEW)- 주의!
Out 메서드의 로직은 모두 실행되지만, 예외가 발생한 Inner 메서드 로직은 RollBack된다.
Out 클래스와 트랜젝션이 분리되었기 때문이다.
- RuntimeException 대신, Exception 로 예외를 발생시킨다.
throw new Exception("에랏 받아랏 Exception 예외다!");- 주의!
Inner 로직, Out 로직 모두 Rollback되지 않고 Commit된다.
단, 예외발생 이후의 로직은 실행되지 않으므로, 로직설계 시 주의가 필요하다.
(즉, 메서드 전체 중 일부는 Commit, exception throws 이후 로직은 아예 실행되지도 않음) - 주의!
checkedException의 속성을 이용한 방법이다. 따라서 잘 이해하고 사용해야한다.
- @Transactionl 어노테이션의 속성에 no-rollback 설정한다.
@Transactional(noRollbackFor = RuntimeException.class)- 주의!
(2번 방법과 동일) Inner 로직, Out 로직 모두Rollback되지 않고, Commit된다.
단, 예외발생 이후의 로직은 실행되지 않으므로 로직설계 시 주의가 필요하다.
(즉, 메서드 전체 중 일부는 Commit, exception throws 이후 로직은 아예 실행되지도 않음)
[다 정리하고 새삼 다시 상기시킨 점]
- 한 트랜잭션에서 롤백된다는 것은 그 트랜잭션 범위의 모든 작업이 롤백되는 것이다(메서드가 몇개 호출되든 가장 바깥 트랜잭션이 있는 메서드 안에서 실행된 모든 로직은 롤백/커밋 둘 중 하나다)
- 만약 트랜잭션이 여러개면 각각 롤백/커밋된다.
재현해보기
- 목표: 내부 로직에서 Exception이 발생하더라도 Out 클래스의 나머지 로직이 실행되어야 한다.
- 시나리오:
- Out 클래스에서 Inner 클래스의 Exception을 발생시키는 메서드를 호출한다.
- Out 클래스에는 3개 로직이 있다.
- 1.직접 리뷰생성 -> 2.예외있는 메서드를 호출 & try-catch에서 예외를 무시 -> 3.직접 리뷰생성
- 결과확인: 시도한 리뷰저장 3개 중 몇개가 저장됐는지 확인한다.
[문제상황 재현] Transaction 전파. Transaction 1개 사용. + 내부 로직에서 RuntimeException 발생
결과 => Out 로직, Inner 로직 모두 RollBack! **(저장된 데이터 0건)**
// Out 클래스
@Slf4j
@RequiredArgsConstructor
@Service
public class OutService {
private final InnerService innerService;
private final ReviewRepository reviewRepository;
@Transactional
public void createReviewWithException() {
// out 로직. 1번리뷰 저장
reviewRepository.save(Review.builder().userId(1L).text("1번리뷰: 요래요래해서 좋았어요!!").build());
try {
// inner 로직 호출. 2번리뷰 저장
innerService.createReviewWithException(2L, "2번리뷰: 요래요래해서 좋았어요!!");
} catch (Exception ex) {
// 의도: exception 받아서 로그만 찍기 때문에 이 메서드의 전체 로직은 정상작동(1,3번 리뷰 저장) 되어야 한다.
log.error("리뷰 생성 중 에러 발생 - " + ex.getMessage());
}
// out 로직. 3번리뷰 저장
reviewRepository.save(Review.builder().userId(3L).text("3번리뷰: 요래요래해서 좋았어요!!").build());
log.info("완료");
}
}
// Inner 클래스
@RequiredArgsConstructor
@Service
public class InnerService {
private final ReviewRepository reviewRepository;
// 저장 + 예외
@Transactional
public void createReviewWithException(Long userId, String text) {
reviewRepository.save(Review.builder().userId(userId).text(text).build());
throw new RuntimeException("에랏 받아랏 RuntimeException 예외다!");
}
}
[재현결과]
- 에러 발생
UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only - Out에서 저장한 거, Inner에서 저장한 거 모두 RollBack 됨

[해결방법1 재현] Inner 로직(예외포함)의 Transaction 분리. Transaction 2개 사용.
결과 => Inner 로직은 Rollback, Out 로직은 Commit 됨 *(저장된 데이터 2건: *OUT에서 저장한 데이터)
public class InnerService {
......
// Inner 클래스의 예외발생 메서드 수정: REQUIRES_NEW 설정해서 별도 트랜젝션으로 실행하도록 함
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void createReviewWithException(Long userId, String text) {
reviewRepository.save(Review.builder().userId(userId).text(text).build());
throw new RuntimeException("에랏 받아랏 RuntimeException 예외다!");
}
}
[재현결과]
- 롤백됐다는 메시지가 표시되지 않는다.
- Inner 로직은 Rollback, Out 로직은 Commit 된다.

[해결방법2 재현] RuntimeException 대신 Exception(CheckedException)으로 발생시킨다.
결과 => OUT, INNER 에서 저장한 모든 로직 Commit 됨 (저장된 데이터 3건: 저장 시도한 모든 데이터 저장)
@Slf4j
@RequiredArgsConstructor
@Service
public class InnerService {
private final ReviewRepository reviewRepository;
@Transactional
public void createReviewWithException(Long userId, String text) throws Exception {
reviewRepository.save(Review.builder().userId(userId).text(text).build());
// throw new RuntimeException("에랏 받아랏 RuntimeException 예외다!");
// RuntimeException 대신 Exception을 발생시킨다.
throw new Exception("에랏 받아랏 Exception 예외다!");
}
}
[재현결과]
- 1,2,3번 리뷰 모두 생성된다.
- Exception 에러는 catch 구문에서 처리되고 Out/Inner 클래스의 모든 로직이 Commit 된다.
- 주의! 만약 예외발생 이후 로직이 있면 주의해야한다!
예외발생 전 로직만 실행되고, 예외발생 후에 작성된 로직은 실행되지 않기 때문이다!!!
[주의사항 상세설명]
@Slf4j
@RequiredArgsConstructor
@Service
public class InnerService {
private final ReviewRepository reviewRepository;
@Transactional
public void createReviewWithException(Long userId, String text) throws Exception {
Review save = reviewRepository.save(Review.builder().userId(userId).text(text).build());
// 만약, 이렇게 조건에 따라 Exception을 발생시켰을 때
if (save != null) {
throw new Exception("에랏 받아랏 Exception 예외다!");
}
// 아래 로직을 실행되지 않는다: 예외발생 이후의 로직은 실행되지 않는다.
Review save2 = reviewRepository.save(Review.builder().userId(userId).text(text).build());
}
}
[해결방법3 재현] @Transactionl 어노테이션의 속성에 rollback하지 말라고 설정한다
- spring의 @Transactional를 쓸 때:
@Transactional(noRollbackFor = RuntimeException.class) - javax의 @Transactional를 쓸 때:
@Transactional(dontRollbackOn = RuntimeException.class)
결과 => OUT, INNER 에서 저장한 모든 로직 Commit 됨 (저장된 데이터 3건: 저장 시도한 모든 데이터 저장)
@Slf4j
@RequiredArgsConstructor
@Service
public class InnerService {
private final ReviewRepository reviewRepository;
// 아래 둘 중 하나 쓰면 됨
@org.springframework.transaction.annotation.Transactional(noRollbackFor = RuntimeException.class)
@javax.transaction.Transactional(dontRollbackOn = RuntimeException.class)
public void createReviewWithException(Long userId, String text) throws Exception {
reviewRepository.save(Review.builder().userId(userId).text(text).build());
throw new RuntimeException("에랏 받아랏 RuntimeException 예외다!");
}
}
[재현결과]
- 2번 방법과 동일한 결과.
- 1,2,3번 리뷰 모두 생성된다.
- @Transactional에 설정한대로 RuntimeException이 발생해도 롤백하지 않는다.
- 주의! 앞 방법과 마찬가지로 만약 예외발생 이후 로직이 있면 주의해야한다!
예외발생 전 로직만 실행되고, 예외발생 후에 작성된 로직은 실행되지 않기 때문이다!!!
좀 더 파보기
왜 롤백되는가?
Spring Doc
Spring Doc의 Transaction 설명 중 rollback-only 관련 문구를 찾을 수 있었다.
여기서는 해결방법1에 관련된 내용을 다시 한 번 확인할 수 있다.
요약하자면,
- 트랜잭션 전략 중 '전파'를 사용하는 경우!
rollback-only 가 기본적으로 적용되어있으며, 내부 트랜잭션에서 발생한 롤백은 외부 트랜잭션에도 영향을 끼친다.
즉, 내부 트랜젝션이 예외발생으로 롤백되면, 외부 트랜젝션도 롤백된다. - 반면 RequiresNew 방법을 사용할 때는 아예 트랜잭션이 별도로 생기므로 외부 트랜잭션에 영향을 끼치지 않는다.
대신, 내부 트랜잭션에서는 롤백이 발생한다.
그럼에도 질문이 생긴다
그럼 왜 rollback-only는 기본값인가?
왜 RuntimeException 일 때만 롤백하는가? 그냥 Exception일 때는 왜 롤백하지 않는가?
오히려 롤백하는게 더 헷갈리지 않나? 일부러 개발자가 try-catch를 명시해서 처리한다는데??
토비의 스프링
요 질문들에 대한 대답은 '토비의 스프링' 책에서 찾을 수 있었다.
앞서 소개한 해결방법 중 2,3번에 대한 이해를 할 수 있다.
참고로 해결방법 2,3번은 트랜잭션 전략=전파다. 즉 1개 트랜잭션 범위이다.
책 내용을 요약하자면..
- rollback-only 기본값 true인 이유 & 해결방법1(checkedException 사용) 근거:
기본적으로 "예외가 발생하면 롤백한다"는 전제를 깐다.
즉, '트랜잭션이 전파'되어 1개 트랜잭션의 범위에 있는 로직이니까 당연히 내부에서 롤백되면, 외부도 롤백되어야 한다는 전제이다.
반면에 checkedException을 발생시키면: 일종의 '비지니스 로직'으로 이식하고 커밋하는 것이다. - 해결방법2(@Transactional 설정) 근거:
바로 윗 근거대로 예외원칙을 항상 따르면 좋겠지만, 사실 'checkedException/uncheckedException 예외원칙'을 따르지 않을 때가 있다.(실무에서 RuntimeException(대표적인 uncheckedException)만 사용할 때도 있다)
즉, Exception 구분으로 처리하기 힘든 상황인 것이다.
이럴 때 사용하라고 TransactionAttribute에서 rollbackOn() 속성을 설정할 수 있게 한 것이다.

checkedException, unCheckedException 차이 조사
추가로 토비의 책에서도 있던 '예외원칙'에 대해 좀 더 알아보자.
여기 잘 정리한 블로그를 보자
'삽질인가 고찰인가' 카테고리의 다른 글
| 변수, 타입 추론 VS 명시적 타입 지정. 그동안 명시적 타입을 선호했는데... '타입 추론'을 지향해야 한다고? (1) | 2025.01.31 |
|---|---|
| java의 stream에 대하여. 책을 읽어도 이해 안되는거 나만...인가? Stream 동작방식 쉽게 설명해보기. (1) | 2025.01.30 |
| Git 명령어) 중간 commit 삭제하기 (0) | 2024.09.20 |
| Python 클래스 메서드, 클래스 속성, 정적 메서드. 개념 구분/용도/주의사항. 언제 어떻게 쓸까? (1) | 2024.08.08 |
| [JUnit] Mockito.verify() 사용할 때 @Autowired vs @MockBean vs @SpyBean (0) | 2023.04.06 |
