백엔드 면접 용어 사전
Java·객체지향Checked 예외

Checked Exception

컴파일러가 처리를 강제하는 예외. 스프링에서는 기본적으로 롤백되지 않는다는 점이 함정이다.

컴파일러가 반드시 처리하도록 강제하는 예외. 처리하지 않으면 컴파일이 안 된다.

계층

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가 유효한 경우

호출자가 정말로 대처할 수 있고, 대처하지 않으면 심각한 문제가 되는 경우 예: 결제 잔액 부족 — 반드시 처리해야 하는 정상 흐름의 일부 드물지만 존재한다. "복구 가능하고, 놓치면 안 되는가" 를 기준으로 판단한다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Java·객체지향 — OOP·JVM·GC·컬렉션