Java Checked Unchecked Exception 차이, Spring @Transactional에 미치는 영향 쉽게 이해하기
Java Checked Unchecked Exception 차이는 중요한 개념인데 기억이 잘 안 나서 다시는 까먹지 않고, 다른 사람에게도 능수능란하게 설명할 수 있도록 쉽게 정리해 보려고 합니다.
프로젝트를 진행하다 보면 @Transactional 어노테이션을 정말 많이 사용하게 됩니다. 그런데 어떤 팀은 기본값 그대로 쓰고, 어떤 팀은 모든 메서드마다 rollbackFor=Exception.class를 붙여놓기도 합니다. 저도 처음엔 “그냥 다 롤백 되게 Exception.class 쓰면 되는 거 아냐?”라고 생각했는데, 이게 생각보다 깊은 이야기였습니다.
오늘은 Java Checked Unchecked Exception 차이를 실무 관점에서 정리하고, @Transactional이 왜 Unchecked Exception만 롤백 하는지, 그리고 어떻게 설정하는 게 가장 합리적인지 알아보겠습니다.

Java Checked Unchecked Exception 차이
Java의 예외는 크게 두 종류로 나뉩니다. 이 구분은 컴파일러가 어떻게 처리하느냐의 차이인데, 실무에서는 트랜잭션 롤백 동작에 직접적인 영향을 줍니다.
Checked Exception은 컴파일 시점에 반드시 처리해야 하는 예외입니다. IOException, SQLException 같은 것들이 여기 속하죠. 코드를 작성할 때 try-catch로 감싸거나 throws 키워드로 던져야 합니다. 안 그러면 컴파일 자체가 안 됩니다. 이런 예외들은 보통 외부 시스템과의 통신이나 파일 처리처럼 “예상 가능한 문제 상황”을 나타냅니다.
Unchecked Exception은 RuntimeException을 상속받은 예외들입니다. NullPointerException, IllegalArgumentException 같은 것들이죠. 이건 컴파일러가 강제하지 않아서 try-catch 없이도 코드가 돌아갑니다. 주로 프로그래밍 실수나 논리적 오류를 나타냅니다.
여기서 핵심은, Spring의 @Transactional은 기본적으로 Unchecked Exception에서만 롤백 한다는 점입니다.
Spring 공식 문서 – Using @Transactional에 보면
Any RuntimeException or Error triggers rollback, and any checked Exception does not.
이라고 확실하게 나와 있습니다.

@Transactional이 Checked Exception을 무시하는 이유
처음 이 사실을 알았을 때 “왜 굳이?”라는 생각이 들었습니다. 예외가 발생했으면 당연히 롤백 해야 하는 거 아닌가요?
Java Tutorial 문서 중 Unchecked Exceptions — The Controversy에 보면 이유가 나와 있습니다.
Checked Exception은 “복구 가능한 상황”으로 간주됩니다. 예를 들어 파일을 읽다가 FileNotFoundException이 발생했다면, 개발자가 catch 블록에서 대체 파일을 읽거나 기본값을 사용하는 식으로 처리할 수 있습니다. 즉, 예외가 발생해도 비즈니스 로직이 정상적으로 완료될 수 있다는 거죠.
반면 Unchecked Exception은 “프로그램의 논리적 오류”를 의미합니다. NullPointerException이 터졌다면 뭔가 코드가 잘못된 거고, 이 상태로 트랜잭션을 커밋 하면 데이터 정합성이 깨질 가능성이 높습니다.
하지만 실무에서는 이 구분이 항상 명확하지 않습니다. 특히 CheckedException 롤백 안되는 이유 때문에 의도치 않은 커밋이 발생하는 경우가 꽤 있습니다.
Spring의 SQLException Unchecked 변환
데이터베이스 작업 중 오류가 발생하면 SQLException이 발생합니다. SQLException은 CheckedException이죠? @Transactional 기본 설정 그대로라면 데이터베이스 작업 중 오류가 발생해도 롤백이 안 되게 됩니다.
Java 초기 설계에서 SQLException은 Checked Exception으로 만들어졌습니다. “데이터베이스 연결 문제는 복구 가능하다”라는 가정이었던 것 같은데, 실제로는 대부분의 SQL 오류는 복구가 불가능합니다. 테이블이 없다거나, 제약조건 위반이라거나, 데드락이 발생했다면 그냥 롤백하고 다시 시도하거나 에러를 반환해야 합니다.
Spring은 이 문제를 인식하고 SQLException Unchecked 변환을 수행합니다. Spring의 DataAccessException이라는 Unchecked Exception으로 감싸서 던지는 거죠. JdbcTemplate이나 Spring Data JPA, MyBatis를 사용하면 SQLException을 직접 볼 일이 거의 없는 이유가 바로 이겁니다.
// Spring이 내부적으로 하는 일
try {
// JDBC 작업
} catch (SQLException e) {
throw new DataAccessException("DB 오류", e); // Unchecked로 변환
}
이렇게 변환된 예외는 Unchecked Exception이므로 @Transactional이 자동으로 롤백 합니다. 개발자가 신경 쓸 필요가 없어지는 거죠.
Spring 공식 문서 – 12. DAO support에 보면 아래와 같이 SQLException을 DataAccessException으로 변환한다는 내용이 나옵니다.
Spring provides a convenient translation from technology-specific exceptions like SQLException to its own exception class hierarchy with the DataAccessException as the root exception. These exceptions wrap the original exception so there is never any risk that one might lose any information as to what might have gone wrong.
Transactional rollbackFor 설정 방법 3가지
그렇다면 실무에서는 어떻게 설정하는 게 좋을까요? 3가지 접근법을 비교해 보겠습니다.
1. 기본값 사용
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
// Unchecked Exception만 롤백
}
가장 간단한 방법입니다. Spring의 예외 변환 메커니즘을 믿고 기본값을 쓰는 거죠. Spring Data JPA나 Spring Data를 사용한다면 대부분의 데이터베이스 예외는 이미 Unchecked로 변환되어 있습니다. 코드도 깔끔하고 Spring의 설계 의도대로 사용하는 방식입니다.
단점은 외부 API 호출이나 파일 처리 같은 작업에서 Checked Exception이 발생하면 롤백 되지 않는다는 점입니다. 이런 경우 명시적인 처리가 필요합니다.
2. 모든 예외 롤백
@Transactional(rollbackFor = Exception.class)
public void processOrder(Order order) {
orderRepository.save(order);
// 모든 Exception 롤백
}
“일단 다 롤백하고 보자”는 접근법입니다. 확실히 안전하긴 합니다. 어떤 예외가 발생하든 트랜잭션이 롤백 되니까 데이터 정합성 문제는 없습니다.
하지만 복구 가능한 Checked Exception까지 롤백 하게 되면, 원래 의도했던 비즈니스 로직이 깨질 수 있습니다. 예를 들어 선택적 알림 전송이 실패했다고 주문 자체를 롤백 하는 건 과할 수 있죠.
3. 상황별 명시적 설정
@Transactional(rollbackFor = {CustomBusinessException.class, IOException.class})
public void processOrder(Order order) throws IOException {
orderRepository.save(order);
notificationService.send(order); // IOException 발생 가능
}
가장 정교한 방법입니다. 비즈니스 로직의 특성에 맞춰 롤백 할 예외를 명시하는 거죠. 코드를 읽는 사람도 “아, 이 메서드는 IOException이 발생하면 롤백 하는구나”를 바로 알 수 있습니다.
실무 권장사항: 어떻게 쓸 것인가
제 경험상 가장 합리적인 Spring Exception 처리 전략은 이렇습니다.
기본적으로는 @Transactional 기본값을 사용합니다. JdbcTemplate이나 Spring Data JPA, MyBatis를 쓴다면 데이터베이스 관련 예외는 이미 적절히 처리되어 있습니다. 코드도 간결하고 프레임워크의 의도대로 동작합니다.
3. 상황별 명시적 설정에 나와있는 것과 같이 하면 좋겠지만 여건이 안 될 수도 있을 것입니다.
그럴 경우에는 @Transactional 기본값 사용과 Checked Exception은 각자 코드에서 적절하게 복구 시도를 하거나 Unchecked Exception으로 변환하는 방법을 추천합니다. 많은 프로젝트 인원들이 비즈니스 로직이 수정될 때마다 rollbackFor도 같이 적절하게 수정해 준다는 보장도 없으므로 이 방법도 괜찮다고 생각합니다.
의도치 않은 커밋을 방지하는 실전 팁
의도치 않은 커밋을 유발하는 사례를 살펴보겠습니다.
@Transactional
public void processPayment(Payment payment) {
paymentRepository.save(payment);
try {
externalPaymentGateway.charge(payment); // IOException 발생 가능
} catch (IOException e) {
log.error("결제 게이트웨이 오류", e);
// 예외를 먹어버림
}
// 결제는 실패했지만 DB에는 성공으로 저장됨!
}
이 코드의 문제는 IOException이 Checked Exception이라 롤백 되지 않는다는 점이 아닙니다. 더 큰 문제는 예외를 catch 해서 먹어버렸다는 겁니다. 이러면 @Transactional 설정과 무관하게 커밋 됩니다. 이런 경우가 없을 것 같지만 프로젝트 하다 보면 catch에서 에러를 먹어버린 상태로 개발 완료했다고 하는 개발자들도 은근히 있으니 주의해야 합니다.
올바른 처리 방법은 이렇습니다.
@Transactional
public void processPayment(Payment payment) throws PaymentException {
paymentRepository.save(payment);
try {
externalPaymentGateway.charge(payment);
} catch (IOException e) {
throw new CustomBusinessException("결제 처리 실패", e); // Unchecked로 변환해서 던짐
}
}
예외를 비즈니스 예외로 변환해서 다시 던지면, 트랜잭션이 롤백 되고 호출자가 적절히 처리할 수 있습니다.
또 다른 함정은 비동기 작업입니다.
@Transactional
public void processOrder(Order order) {
orderRepository.save(order);
CompletableFuture.runAsync(() -> {
// 다른 스레드에서 실행
throw new RuntimeException(); // 이 예외는 트랜잭션에 영향 없음!
});
}
@Transactional은 같은 스레드 내에서만 동작합니다. 비동기 작업의 예외는 메인 트랜잭션에 영향을 주지 않으니 주의해야 합니다.
자주 묻는 질문 (FAQ)
Exception.class로 통일하면 안 돼요?
안전해 보이지만, 복구 가능한 예외까지 롤백 할 수 있습니다. 당장의 편의성 때문에 사용될 수 있지만, 장기적으로는 코드의 의도를 불분명하게 하고, 유연한 예외 처리를 방해할 가능성이 있습니다.
rollbackFor에는 Checked Exception만 넣어야 해?
rollbackFor에는 Checked Exception만 넣어야 합니다. Unchecked Exception은 넣어도 아무 의미가 없습니다.
RuntimeException을 상속받은 커스텀 예외는 자동으로 롤백 되나요?
네, RuntimeException을 상속받았다면 Unchecked Exception이므로 별도 설정 없이 롤백 됩니다.
noRollbackFor 옵션은 언제 쓰나요?
특정 Unchecked Exception에서는 롤백 하지 않고 싶을 때 사용합니다. 예를 들어 로깅 실패 같은 부수적인 작업의 예외는 무시하고 싶을 수 있습니다.
hibernate는 SQLException을 어떻게 처리하나요?
hibernate는 SQLException을 JDBCException이라는 Unchecked Exception으로 변환합니다.
마무리하며
Java Checked Unchecked Exception 차이를 이해하는 건 단순히 이론을 아는 것 이상입니다. @Transactional의 동작 방식을 제대로 알고, 데이터 정합성을 지키며, 의도한 대로 트랜잭션을 제어하는 실무 능력으로 이어집니다.
핵심을 정리하면 이렇습니다. Spring은 기본적으로 Unchecked Exception에서만 롤백 합니다. 하지만 SQLException 같은 문제적인 Checked Exception은 DataAccessException으로 변환해서 자동으로 롤백 되게 만들어줍니다. 그래서 JdbcTemplate이나 Spring Data JPA, MyBatis를 쓴다면 대부분 기본값으로 충분합니다.
다만 특별한 비즈니스 로직이 있거나 Checked Exception이 발생할 가능성이 있다면, rollbackFor를 명시적으로 설정하는 게 좋습니다. 이때 Exception.class로 모든 걸 롤백 하기보다는, 실제로 롤백이 필요한 예외만 지정하는 게 더 정교한 접근법입니다.
가장 중요한 건 팀 내에서 일관된 규칙을 정하고, 코드 리뷰를 통해 서로의 의도를 명확히 하는 겁니다. “왜 여기는 rollbackFor를 썼지?” 같은 질문이 자연스럽게 나오는 문화가 만들어지면, 의도치 않은 커밋 같은 문제는 크게 줄어들 것입니다.