Spring Boot와 kotlin을 사용하는 개발자들이 @Transactional 어노테이션을 통해 데이터베이스 트랜잭션을 관리합니다.
저도 위와 같은 개발 스택으로 개발을 하고있는데...
분명히 예외가 발생했는데도 데이터가 롤백되지 않는 경우를 겪었습니다.
"아니 @Transactional 달았고 예외도 났는데 왜 DB에 저장된거지..."
제가 놓친 부분에 대해, try-catch 블록이 롤백을 방했던 경험을 정리했습니다.

1. 기본적으로 예외 발생 시 롤백된다.
- unchecked exception (RuntimeException 등) 이 발생하고,
- 그 예외가 메서드 바깥으로 전파될 때
예시로
<kotlin />
@Transactional
fun createUser(name: String?) {
if (name == null) throw IllegalArgumentException("이름은 필수임당") // RuntimeException
userRepository.save(User(name))
}
IllegalArgumentException은 RuntimeException의 하위이므로 롤백 대상이며, 예외도 밖으로 전파되기 때문에 트랜잭션은 롤백됩니다.
하지만
2. Try-catch가 예외를 잡으면?
<kotlin />
@Transactional
fun registerUser(name: String?) {
try {
createUser(name) // 내부에서 RuntimeException 발생
} catch (e: IllegalArgumentException) {
println("에러 발생: ${e.message}")
// 예외를 다시 던지지 않음!
}
}
예외가 catch 블록에서 잡히고 끝났기 때문에,
spring 입장에서는 "정상적으로 끝났네?" 라고 생각합니다.
그래서 트랜잭션을 커밋합니다.
즉, rollback 안됨
AOP 기반 트랜잭션의 한계
spring은 예외가 '메서드 바깥으로 전달되는지 여부'를 보고 롤백할지 말지를 판단합니다.
catch 블록에서 처리되고 다시 던져지지 않으면, Spring은 에러가 없다고 판단하고 commit 하게 되는것이죠
3. 문제를 처리했던 경험
예외를 catch 했더라도, 다시 throw 해주면 트랜잭션은 롤백되지만, 에러 후 catch 부분에 다시 throw하는 코드는 개인적으로 이해하기 힘들어서 강제로 롤백 플래그를 설정해주었습니다.
<kotlin />
import org.springframework.transaction.interceptor.TransactionAspectSupport
@Transactional
fun registerUser(name: String?) {
try {
createUser(name)
} catch (e: IllegalArgumentException) {
println("에러 발생: ${e.message}")
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
}
}
이렇게 하면 예외를 다시 던지지 않아도 트랜잭션은 롤백됩니다.
강제적으로 rollback 하는 코드를 사용하는것은 상황에 따라 적절하다고 생각합니다.
3.1. 공부하면서 정리 한 표 공유
상황 | 기본 롤백 여부 | 커스터마이징 |
RuntimeException 던짐 | 롤백 O | O |
Checked Exception 던짐 | 롤백 X | rollbackFor 설정 |
예외를 try-catch로 잡고 끝냄 | 롤백 X | setRollbackOnly 가능 |
rollbackFor로 지정된 예외 발생 | 롤백 O | - |
noRollbackFor로 지정된 예외 발생 | 롤백 X | - |
글에서 정리된 부분은 아니지만 궁금하신분은 찾아보시면 도움될겁니다.
3.2. 참고
Rolling Back a Declarative Transaction :: Spring Framework
You must carefully consider how specific a pattern is and whether to include package information (which isn’t mandatory). For example, "Exception" will match nearly anything and will probably hide other rules. "java.lang.Exception" would be correct if "E
docs.spring.io
감사합니다.
'Back End > Spring' 카테고리의 다른 글
JPA 지연로딩 (Lazy Loading)과 N+1 문제 정리 - fetchJoin, DTO활용 (0) | 2025.04.09 |
---|---|
LocalDateTime과 ZonedDateTime 시간, UTC 타임존 적용하기 (3) | 2024.10.28 |
Spring Security UsernamePasswordAuthenticationToken 사용해보자 (2) | 2024.07.25 |
JPA와 하이버네이트: 객체-관계 매핑의 장점과 비교 (29) | 2024.03.09 |
Spring Boot 프로젝트 생성: Spring Initializer의 활용법 (0) | 2024.01.12 |