Java 체크 예외(checked)와 언체크 예외(unchecked) — 올바른 예외 처리 선택 가이드

검증 환경

본 절은 원문 사례·문서에 등장한 버전/도구를 정리한 것이다. 별도 실험실 재현이 명시되지 않은 항목은 일반화하지 않는다.

  • 본문에 명시된 오류 메시지·프레임워크 버전을 기준으로 원인을 추적했다.
  • 문서 정리일: 2026-09-03
  • OS/CI 세부 값은 프로젝트마다 다르므로, 적용 전 로컬에서 동일 오류 메시지를 재확인한다.

1. 문제 정의

Java에서 예외 처리 코드를 처음 작성하면 아래와 같은 혼란이 생깁니다.

1
2
3
4
5
6
try {
    String userInput = //read in user input
    Long id = Long.parseLong(userInput);
} catch (NumberFormatException e) {
    id = 0; //recover the situation by setting the id to 0
}

위 코드의 NumberFormatException체크 예외일까요? 그리고 아래처럼 파일을 열 때 FileNotFoundException을 잡는 코드는 무엇을 해야 할까요?

1
2
3
4
5
6
7
8
9
try {
    File file = new File("my/file/path");
    FileInputStream fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
    // 3. What should I do here?
    //Should I "throw new FileNotFoundException("File not found");"?
    //Should I log?
    //Or should I System.exit(0);?
}

같은 “예외"라는 이름이지만 체크 예외와 언체크 예외는 컴파일러의 강제 여부의도가 다릅니다. 이 차이를 이해하지 못하면 예외를 삼키거나, 전혀 엉뚱한 레이어에서 잡거나, 더 나쁘게는 예외 때문에 프로그램을 종료해버리는 실수를 하게 됩니다.

2. 먼저 확인할 것 — 체크/언체크 예외 판별 기준

예외를 잡기 전에 먼저 내가 마주한 예외가 어느 종류인지 판별해야 합니다. 판별 기준은 RuntimeException을 상속했는가 한 가지입니다.

1
2
3
4
5
6
7
8
// 문제 1번에 대한 답
// NumberFormatException 은 RuntimeException 의 하위 클래스입니다.
NumberFormatException nfe = new NumberFormatException();
boolean isRuntime = nfe instanceof RuntimeException;   // true

// FileNotFoundException 은 RuntimeException 을 상속하지 않습니다.
FileNotFoundException fnfe = new FileNotFoundException();
boolean isRuntime2 = fnfe instanceof RuntimeException; // false

판별은 소스에서 상속 관계를 확인하면 됩니다.

예외직접 상위 타입RuntimeException 상속?컴파일러 강제(catch/throws)종류
NumberFormatExceptionIllegalArgumentException✔ 예아니오언체크 예외
FileNotFoundExceptionIOException✘ 아니오체크 예외
IOExceptionException✘ 아니오체크 예외
NullPointerExceptionRuntimeException✔ 예아니오언체크 예외
ArithmeticExceptionRuntimeException✔ 예아니오언체크 예외

증상 fingerprint

  • 에러 메시지: compiler error: unreported exception <Type>; must be caught or declared to be thrown (체크 예외에서 발생)
  • 발생 단계: 컴파일 타임 (checked) / 런타임 (unchecked)
  • 관련 도구: javac, IDE (IntelliJ/Eclipse)
  • 흔한 오해: “try-catch로 잡으면 전부 체크 예외다” → 이는 오류입니다. catch는 체크 예외도 언체크 예외도 모두 할 수 있으며, 체크/언체크는 컴파일러가 처리 강제 여부로 구분합니다. NumberFormatException도 코드에 따라 catch할 수 있지만 그것은 “잡곘다"는 개발자의 선택일 뿐입니다.

3. 원인별 분기표 — “내 상황이면 무엇을 해야 하나?”

예외 처리 선택은 코드가 어느 레이어에 있는지와 복구 가능성에 따라 나뉩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
예외가 발생했다
├─ (A) 복구 가능한 상황인가?
│      ("잘못된 사용자 입력", "파일 없음 → 다시 입력")
│      │
│      └─ 예 → 체크 예외를 쓸 수 있음 + 이 레이어가 잡을지 판단
├─ (B) 프로그래밍 실수인가? (null 접근, 잘못된 포맷 변환)
│      -> 언체크 예외(RuntimeException) - 버그이므로 고친다
레이어 기준 처리 방향
├─ UI 레이어  → catch 해서 사용자에게 경고를 보여준다
├─ 서비스 레이어 → catch 하지 말고 그대로 위로 전파(bubble) 시킨다
└─ 어느 경우든 예외를 삼키지(smallow) 않는다
상황예외 종류처리 위치해결 방향
사용자가 숫자를 잘못 입력, Long.parseLong 실패NumberFormatException(언체크)UI 레이어catch 후 사용자에게 경고 메시지 표시
파일이 없는 경로로 파일 열기FileNotFoundException(체크)UI 레이어catch 후 “파일 경로를 다시 입력” 요청
파일 열기 실패FileNotFoundException(체크)서비스 레이어catch하지 않고 그대로 위로 전파
parseLong 뒤 로직 파라미터를 잘못 넘김언체크(프로그래밍 오류)어디든버그이므로 catch 하지 말고 수정

핵심 분기 기준

  1. 이 예외가 “복구 가능한 상황”(recoverable condition)을 나타내는가? → 예라면 체크 예외를 사용하고, 그 상황을 해결하는 게 맞습니다.
  2. 이 예외가 “프로그래밍 실수”(programming error)를 나타내는가? → 이렇다면 RuntimeException(언체크)입니다. Joshua Bloch의 《Effective Java》(2판 Item 58)의 권장과 동일합니다.
  3. 내 코드가 어느 레이어인가? → UI 레이어면 catch해서 경고, 서비스 레이어면 catch 하지 말고 전파(bubble), 그리고 절대로 삼키지 않습니다.

4. 상황별 해결책 — 코드로 구현

상황 A) UI 레이어에서 복구 가능한 언체크 예외 잡기

사용자 입력이 잘못된 숫자여서 발생한 NumberFormatException은 복구 가능합니다. UI 레이어에서는 잡아서 경고를 보여주면 됩니다.

1
2
3
4
5
6
7
try {
    Long id = Long.parseLong(userInput);
    // ... id 사용 로직
} catch (NumberFormatException e) {
    // UI 레이어: 사용자에게 경고 표시
    showWarning("올바른 숫자를 입력해 주세요.");
}

상황 B) UI 레이어에서 체크 예외 잡고 재요청

파일을 찾을 수 없을 때, UI에서 사용자에게 다시 경로를 요청하는 것이 자연스럽다면 이렇게 처리합니다.

1
2
3
4
5
6
7
8
try {
    String filePath = //read in from user input file path
    File file = new File(filePath);
    FileInputStream fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
    //Kindly prompt the user an error message
    //Somehow ask the user to re-enter the file path.
}

상황 C) 서비스 레이어에서 catch 하지 말고 전파(bubble) 하기

서비스 로직에서 파일이 없는 경우, 그 코드에서는 사용자에게 뭘 보여줄지 결정할 수 없습니다. 이때는 관련 정보를 손실하지 않고 그대로 위로 전파합니다. 예외를 삼키지 않으면서 상위 레이어(UI)에 결정을 넘기는 것이 블로그 접근입니다.

1
2
3
4
5
// 파일을 여는 서비스 메서드 - 여기서 결정하지 않는다
public void processFile(String path) throws FileNotFoundException {
    FileInputStream fis = new FileInputStream(new File(path));
    // ...
}

FileNotFoundException이 checked이므로 throws 선언은 필수입니다. 이것은 불필요한 강제가 아니라 “이 메서드를 호출하는 쪽이 파일 없음 상황을 알아야 한다"라는 계약입니다.

예외를 못 할 때 취할 수 있는 옵션 3가지

대부분의 경우 예외가 발생하면 아래 중 하나를 선택합니다.

  1. 로그를 남기고(log it) 정상 경로로 return
  2. 메서드에 throws를 선언해 재throw(rethrow)
  3. 현재 예외를 생성자에 넘겨 새 예외로 감싸기
1
2
3
4
5
6
// 옵션 3) 예외를 새 예외로 감싸 전파 - 원인(cause)을 보존
try {
    FileInputStream fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
    throw new MyAppException("파일을 열 수 없습니다", e); // cause 보존(wrapping)
}

잘못된 해결책 (wrong fix) System.exit(0)으로 프로그램을 종료시키기, 예외를 잡고 아무것도 하지 않고 빈 catch를 남기기(swallowing)는 모두 피해야 합니다. 예외를 삼키면 상위 레이어는 실패 원인을 전혀 알 수 없어 디버깅이 불가능해지고, System.exit은 전체 애플리케이션(서버 포함) 실행환경을 종료시켜 버립니다. 원하는 것은 “파일 하나 못 열었을 뿐"인데도 프로그램이 죽는 결과를 만듭니다.

5. 검증 명령

아래 코드가 예상대로 통과하면 체크/언체크 구분이 올바르게 이해된 것입니다.

1
2
3
4
5
# javac로 체크 예외 선언 누락 시 컴파일 오류가 나는지 확인
javac CheckedUncheckedDemo.java
# 성공 시 아무 출력 없음(컴파일 통과)
# 체크 예외를 throws/처리 없이 둔다면:
#   error: unreported exception java.io.FileNotFoundException; must be caught or declared to be thrown

컴파일을 통과하기 위해 아래 구조 마저 만들어 둡니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import java.io.*;

public class CheckedUncheckedDemo {
    public static void main(String[] args) {
        try {
            Long id = Long.parseLong("abc");   // RuntimeException (unchecked)
        } catch (NumberFormatException e) {
            System.out.println("parsed as default: 0");   // UI 처럼 처리됨
        }
    }

    // 체크 예외는 throws 선언으로 전파하고, 호출부가 결정
    static void readFile(String path) throws FileNotFoundException {
        FileInputStream fis = new FileInputStream(new File(path));
    }
}
1
2
javac CheckedUncheckedDemo.java && java CheckedUncheckedDemo
# 출력: parsed as default: 0

체크 예외 throws를 선언에서 제거하면 javac가 오류를 내므로, 자신의 판단이 맞는지 바로 확인할 수 있습니다.

6. 예방 조치 — 재발 방지를 위한 습관

  1. 예외를 삼키지 않는다 — 빈 catch 블록은 절대 두지 않습니다.
  2. 레이어별 책임을 정한다 — UI 레이어는 사용자에게 경고, 서비스 레이어는 전파(bubble)만 담당.
  3. 복구 가능한 곳에서만 잡는다 — 복구 UI가 있는 곳(주로 UI 레이어)에서만 catch하고, 그 외에는 버블링.
  4. effective Java 원칙 기억하기 — “checked exception은 복구 가능한 상황, RuntimeException은 프로그래밍 실수”.
  5. catch 순서 주의 — 체크 예외와 언체크 예외를 함께 잡을 때는 구체 예외(하위)를 먼저, 일반 예외(상위)를 나중에 둡니다.

DevTrace verdict 복구 가능한 상황은 UI에서 catch 경고하고, 프로그래밍 오류는 언체크로 두되 절대 삼키지 말고 확실히 결정된 레이어에서 처리한다는 점이다.


출처

DevTrace 결론

핵심은 체크 예외는 복구 가능한 상황에서, 언체크 예외는 프로그래밍 실수에 사용하고, 어느 레이어에서 잡느냐에 따라 처리 방식을 달리 한다는 점이다.

원문 출처는 문제 발견의 단서이며, 위 판단과 점검 항목은 DevTrace의 독자 분석이다.