자바(Java) 백엔드 개발자 면접에서 가장 변별력 있는 질문 중 하나이자, 실무에서 데이터 불일치(Data Inconsistency) 문제를 일으키는 주범인 “예외 처리와 트랜잭션 롤백 전략“에 대한 포스팅입니다.
요청하신 Checked Exception과 Unchecked Exception의 차이 및 올바른 트랜잭션 처리 전략을 주제로, 애드센스 승인용 고품질 포스팅을 작성해 드립니다.
Java 예외 처리의 핵심: Checked vs Unchecked 차이와 트랜잭션 롤백 전략
1. 서론
자바 개발을 하다 보면 수많은 예외(Exception)를 마주하게 됩니다. 어떤 에러는 빨간 줄이 그어지며 “반드시 try-catch로 처리하라”고 강요받는 반면, 어떤 에러는 아무런 경고 없이 빌드되었다가 실행 중에 서버를 터뜨리기도 합니다. 도대체 이 둘의 기준은 무엇일까요? 더 심각한 문제는 이 예외의 종류에 따라 데이터베이스의 트랜잭션(Transaction) 롤백 여부가 달라진다는 사실입니다.
이 차이를 명확히 알지 못하면, 예외가 발생했음에도 불구하고 데이터가 DB에 저장되어버리는 치명적인 버그를 만들 수 있습니다. 이는 금전적인 손실이나 데이터 무결성 훼손으로 이어집니다. 오늘은 자바의 예외 계층 구조를 통해 Checked Exception과 Unchecked Exception의 결정적 차이를 이해하고, 이를 바탕으로 스프링 환경에서 안전하게 트랜잭션을 관리하는 실무 전략까지 심도 있게 알아보겠습니다.
2. 본론
1: 자바 예외 계층 구조와 두 예외의 정의
자바에서 모든 예외의 조상은 java.lang.Throwable 클래스입니다. 이 아래에 시스템 레벨의 심각한 문제를 뜻하는 Error(예: OutOfMemoryError)와 애플리케이션 레벨의 Exception으로 나뉩니다. 우리가 집중해야 할 곳은 Exception 하위 클래스들입니다.
1. Checked Exception (컴파일 예외)
- 정의:
Exception클래스를 상속받되,RuntimeException을 상속받지 않은 예외들입니다. - 특징: 컴파일러가 예외 처리를 강제합니다. 즉, 개발자가
try-catch로 잡거나throws로 던지지 않으면 컴파일 에러가 발생하여 실행조차 할 수 없습니다. - 의도: “이 예외는 발생할 가능성이 높으니, 개발자가 반드시 예측하고 대처 로직을 작성하라”는 의미입니다.
- 예시:
IOException,SQLException,ClassNotFoundException등 외부 시스템(파일, DB)과 통신할 때 주로 발생합니다.
2. Unchecked Exception (런타임 예외)
- 정의:
RuntimeException을 상속받은 모든 예외들입니다. - 특징: 컴파일러가 예외 처리를 강제하지 않습니다. 명시적인 처리가 없어도 빌드와 실행이 가능합니다.
- 의도: “이것은 개발자의 실수로 발생한 에러다.” 즉, 미리 예측해서 복구하기보다는 코드를 수정해서 해결해야 할 오류들입니다.
- 예시:
NullPointerException,IllegalArgumentException,IndexOutOfBoundsException등.
2: 한눈에 보는 비교 분석 (표)
이 두 예외의 차이는 면접 단골 질문이므로 아래 표로 확실히 정리해두는 것이 좋습니다.
| 구분 | Checked Exception | Unchecked Exception |
| 상속 | Exception 상속 | RuntimeException 상속 |
| 확인 시점 | 컴파일 단계 (Compile-time) | 실행 단계 (Runtime) |
| 예외 처리 | 필수 (Mandatory) | 선택 (Optional) |
| 트랜잭션 처리 | 기본적으로 롤백 안 함 (Commit) | 기본적으로 롤백 함 (Rollback) |
| 대표 예시 | IOException, SQLException | NPE, IllegalArgumentException |
3: 치명적인 함정, 트랜잭션 롤백과 예외의 관계
스프링 프레임워크에서 @Transactional 어노테이션을 사용할 때 가장 주의해야 할 점이 바로 “Checked Exception은 기본적으로 롤백되지 않는다“는 것입니다.
스프링의 트랜잭션 관리자는 기본 정책(Default Policy) 상 Unchecked Exception(런타임 예외)이 발생하면 “복구 불가능한 심각한 오류”로 판단하여 트랜잭션을 롤백합니다. 하지만 Checked Exception이 발생하면 “예측 가능하고 복구 가능한 예외“로 간주하여, 예외가 터졌음에도 불구하고 커밋(Commit)을 수행해 버립니다.
[잘못된 코드 예시]
Java
@Transactional
public void transferMoney(String accountId, int amount) throws Exception {
accountRepository.withdraw(accountId, amount); // 1. 출금 실행
// 2. 만약 여기서 Checked Exception인 IOException이 발생한다면?
if (isSystemError()) {
throw new IOException("파일 시스템 오류");
}
// 결과: 롤백되지 않고 출금 내용이 DB에 반영됨 (돈은 빠져나갔는데 에러 발생)
}
위 코드에서 IOException이 발생해도 트랜잭션은 롤백되지 않습니다. 사용자의 돈은 출금되었는데 시스템은 에러 화면을 보여주는 최악의 상황이 발생합니다.
4: 실무에서의 올바른 트랜잭션 처리 전략 2가지
이러한 문제를 해결하고 데이터의 정합성을 지키기 위해 실무에서는 다음 두 가지 전략을 사용합니다.
전략 1: Checked Exception을 Unchecked Exception으로 변환하여 던지기 (권장)
최근 모던 자바/스프링 개발의 트렌드는 “모든 예외를 런타임 예외(Unchecked)로 처리하자“입니다. 복구할 수 없는 예외(DB 연결 실패, SQL 문법 오류 등)를 굳이 상위 메서드까지 줄줄이 throws로 끌고 갈 필요가 없기 때문입니다.
try-catch로 Checked Exception을 잡은 뒤, 이를 커스텀 런타임 예외로 감싸서(Wrapping) 다시 던지면 트랜잭션이 알아서 롤백됩니다.
Java
@Transactional
public void saveUser(UserDto dto) {
try {
// Checked Exception 발생 가능 로직
fileService.uploadProfileImage(dto.getImage());
} catch (IOException e) {
// [핵심] 런타임 예외로 감싸서 던짐 -> 자동 롤백 발생
throw new FileUploadException("이미지 업로드 실패", e);
}
userRepository.save(dto.toEntity());
}
전략 2: rollbackFor 옵션 명시하기
부득이하게 Checked Exception을 던져야 한다면, @Transactional 어노테이션에 rollbackFor 옵션을 사용하여 “이 예외가 발생하면 롤백해 주세요”라고 명시적으로 알려줘야 합니다.
Java
// Exception 클래스와 그 하위 모든 예외 발생 시 롤백 수행
@Transactional(rollbackFor = Exception.class)
public void updateProduct(Long id) throws Exception {
// 비즈니스 로직
throw new IOException("강제 예외"); // 이제 롤백됨
}
5: 왜 최근에는 Unchecked Exception을 선호할까?
과거(EJB 시절)에는 견고한 프로그램을 위해 Checked Exception을 강제했습니다. 하지만 실제 개발을 하다 보니 다음과 같은 문제점들이 드러났습니다.
- 무의미한 throws의 전파: DB 연결 오류(
SQLException)가 났을 때 컨트롤러나 서비스 계층에서 할 수 있는 일이 없습니다. 그저 에러 메시지를 띄우는 것뿐입니다. 그런데도 모든 메서드에throws SQLException을 붙여야 하니 코드가 지저분해집니다. - 캡슐화 파괴: 서비스 계층이 데이터 접근 기술(JDBC, JPA 등)의 예외(SQLException)를 알게 되어 의존성이 생깁니다.
따라서 Spring JDBC나 JPA(Hibernate) 등 최신 라이브러리들은 내부적으로 발생하는 SQLException을 DataAccessException이라는 런타임 예외로 포장해서 던져줍니다. 덕분에 개발자는 필요한 경우에만 예외를 잡아서 처리하고, 그렇지 않으면 전역 예외 처리기(@ControllerAdvice)에 맡기면 됩니다.
3. 결론
자바의 예외 처리는 단순히 에러를 잡는 기술을 넘어, 데이터의 운명을 결정짓는 중요한 설계 요소입니다.
오늘 내용을 요약하자면 다음과 같습니다.
- Checked Exception은 컴파일 시점에 확인되며, 트랜잭션 롤백이 안 됩니다.
- Unchecked Exception은 런타임 시점에 확인되며, 트랜잭션 롤백이 됩니다.
- 실무에서는 데이터 정합성을 위해 예외 발생 시 반드시 롤백이 되어야 하므로, Checked Exception을 런타임 예외로 전환(Wrapping)하거나
rollbackFor옵션을 필수로 사용해야 합니다.
안정적인 백엔드 시스템을 구축하기 위해, 습관적인 try-catch 대신 예외의 성격을 파악하고 트랜잭션 범위를 고려하는 ‘똑똑한 예외 처리’를 하시기 바랍니다. 이 작은 차이가 시스템의 신뢰도를 결정합니다.