컴파일러가 반드시 처리하도록 강제하는 예외. 처리하지 않으면 컴파일이 안 된다.
계층
Throwable
├── Error ← 복구 불가 (OutOfMemoryError, StackOverflowError)
└── Exception
├── RuntimeException ← Unchecked. 처리 강제 없음
│ ├── NullPointerException
│ ├── IllegalArgumentException
│ └── IllegalStateException
└── 그 외 모든 Exception ← Checked. 처리 강제
├── IOException
└── SQLException
RuntimeException을 상속하면 unchecked, Exception만 상속하면 checked. 이 한 줄이 전부다.
처리 방법 두 가지
// ① try-catch 로 처리
try { Files.readString(path); }
catch (IOException e) { … }
// ② throws 로 위임
void read() throws IOException { Files.readString(path); }
원래 의도
- Checked — : 호출자가 복구할 수 있는 예외 → 대처를 강제하자
-
(파일이 없다 → 다른 경로를 시도한다) Unchecked : 프로그래밍 오류 → 코드를 고쳐야지 잡을 것이 아니다
-
(null 참조, 잘못된 인자)
-
의도는 훌륭했다. 그런데 실제로는 문제가 많았다.
왜 현대 자바에서 외면받는가
① 대부분 복구 불가능하다
try { jdbcTemplate.query(…); }
catch (SQLException e) {
// DB가 죽었는데 여기서 무엇을 복구하나?
throw new RuntimeException(e); // 결국 이렇게 된다
}
② 예외 삼키기를 유발한다
try { … } catch (IOException e) { } // 컴파일만 통과하면 되니 비워 둔다
가장 위험한 코드다. 문제가 조용히 사라져 나중에 엉뚱한 곳에서 터진다.
③ 시그니처가 전염된다
void a() throws IOException
void b() throws IOException // a를 부르니 어쩔 수 없이
void c() throws IOException // b를 부르니 어쩔 수 없이
저수준 구현 세부사항이 상위 계층 API까지 오염시킨다. 캡슐화를 깨는 것이다.
④ 람다·스트림과 맞지 않는다
list.stream().map(f -> readFile(f)) // ✗ Function은 checked를 던질 수 없다
함수형 인터페이스 대부분이 checked 예외를 선언하지 않아, 람다 안에서 try-catch로 감싸야 한다. 코드가 크게 지저분해진다.
스프링의 선택
// JDBC의 SQLException(checked) → DataAccessException(unchecked)로 변환
try { … } catch (SQLException e) { throw translate(e); }
스프링은 데이터 접근 예외를 전부 unchecked로 감싼다.
@Transactional도 기본적으로 unchecked 예외에만 롤백한다.
@Transactional
public void save() throws IOException {
repo.save(entity);
throw new IOException(); // checked → 롤백되지 않는다! ★
}
@Transactional(rollbackFor = Exception.class) // 명시해야 롤백
★ 이것이 실무에서 매우 자주 걸리는 함정이다. "예외가 났는데 데이터가 저장돼 있다"의 원인이 대개 이것이다.
현대적 지침
✅ 원칙적으로 unchecked 를 쓴다 ✅ 도메인 예외를 정의한다 (OrderNotFoundException extends RuntimeException) ✅ 저수준 예외는 잡아서 의미 있는 예외로 변환한다 (예외 번역) ✅ 원인 예외를 반드시 포함한다 — new MyException("...", e) ❌ 예외를 빈 catch 로 삼키지 않는다 ❌ Exception 을 통째로 잡지 않는다
코틀린에는 checked exception이 아예 없다. 스칼라·C#도 마찬가지다. 자바의 이 실험은 널리 계승되지 않았다.
그래도 checked가 유효한 경우
호출자가 정말로 대처할 수 있고, 대처하지 않으면 심각한 문제가 되는 경우 예: 결제 잔액 부족 — 반드시 처리해야 하는 정상 흐름의 일부 드물지만 존재한다. "복구 가능하고, 놓치면 안 되는가" 를 기준으로 판단한다.