Checked Exception과 Unchecked Exception 어떤 차이가 있을까?

정다운·2026년 9월 17일

자바에서 예외를 공부하다 보면 Checked Exception과 Unchecked Exception이라는 구분이 나온다.

처음에는 단순히 이름만 보고 Checked는 검사하는 예외이고, Unchecked는 검사하지 않는(?) 예외라고 생각했다.

그래서 직접 코드를 실행하면서 어디에서 차이가 생기는지 확인해봤다.

먼저 무엇을 검사한다는 건지

Checked와 Unchecked를 구분하는 기준은 컴파일러가 예외를 잡거나 전달한다고 선언하도록 강제하는지다.

  • Checked Exception: catch로 잡거나 throws로 선언해야 컴파일된다.
  • Unchecked Exception: 둘 다 없어도 컴파일된다.

여기서 검사하는 것은 예외가 실제로 발생할지 여부가 아니다. 예외가 발생할 수 있는 코드에 대해 필요한 처리를 작성했는지다.

실제 예외는 두 종류 모두 실행 중에 발생한다.

예를 들어 파일을 읽는 코드가 있다면, 컴파일러가 파일의 존재 여부까지 확인하는 것은 아니다. 파일 읽기 과정에서 발생할 수 있는 Checked Exception을 잡거나 전달하도록 작성했는지 확인한다.

실습 준비

Checked Exception은 IOException, Unchecked Exception은 NullPointerException으로 비교했다.

1단계. Checked Exception을 처리하지 않음

먼저 아무런 처리 없이 IOException을 발생시켜봤다.

public static void main(String[] args) {
    System.out.println("1. 실행 시작");

    throw new IOException("파일 읽기에 실패했습니다.");
}

결과는 컴파일 오류다.

IOException은 Checked Exception인데 catch로 잡지도 않았고 throws로 선언하지도 않았기 때문이다.

여기서 눈여겨볼 부분은 "1. 실행 시작"도 출력되지 않는다는 점이다. 실행하다가 예외 때문에 멈춘 것이 아니라, 컴파일을 통과하지 못해서 실행 단계로 넘어가지 못했다.

2단계. try-catch로 직접 처리

이번에는 같은 코드를 try-catch로 감쌌다.

public static void main(String[] args) {
    System.out.println("1. 실행 시작");

    try {
        throw new IOException("파일 읽기에 실패했습니다.");
    } catch (IOException e) {
        System.out.println("2. 예외 처리: " + e.getMessage());
    }

    System.out.println("3. 실행 끝");
}

실행 결과:

1. 실행 시작
2. 예외 처리: 파일 읽기에 실패했습니다.
3. 실행 끝

이번에는 컴파일도 되고 마지막 출력까지 실행된다.

throw에서 예외가 발생하면 실행 흐름이 catch로 이동한다. 여기서 예외를 잡고 안내 메시지를 출력한 다음, try-catch 아래의 코드로 진행한다.

예외가 발생하지 않은 것이 아니라 발생한 예외를 잡아서 처리한 것이다.

3단계. catch 대신 throws만 사용

예외를 직접 잡지 않고, 메서드에 throws IOException을 붙여봤다.

public static void main(String[] args) throws IOException {
    System.out.println("1. 실행 시작");

    throw new IOException("파일 읽기에 실패했습니다.");
}

컴파일은 성공한다. 하지만 실행하면 다음 메시지 이후에 IOException 정보가 출력되며 종료된다.

1. 실행 시작

처음에는 throws를 붙이면 예외 처리가 된다고 생각하기 쉬운데 예외 자체가 해결되는 것은 아니다.

throws는 이 메서드에서 해당 예외가 밖으로 전달될 수 있다는 선언이다. 이 예제에서는 main 밖으로 전달된 예외를 잡는 곳이 없어서 main 스레드가 종료된다.

비슷하게 생긴 두 키워드도 역할이 다르다.

  • throw: 실제로 예외를 던진다.
  • throws: 메서드 밖으로 전달될 수 있는 예외를 선언한다.

컴파일에 성공하는 것과 실행 중 발생한 문제에 대응하는 것은 별개였다.

4단계. Unchecked Exception을 처리하지 않음

이번에는 null인 변수로 메서드를 호출해봤다.

public static void main(String[] args) {
    System.out.println("1. 실행 시작");

    String text = null;
    System.out.println("2. null 저장 완료");

    System.out.println(text.length());

    System.out.println("3. 실행 끝");
}

catch도 throws도 없지만 컴파일된다.

실행하면 여기까지 출력된다.

1. 실행 시작
2. null 저장 완료

그다음 text.length()에서 NullPointerException이 발생한다. "3. 실행 끝"은 출력되지 않는다.

text의 타입은 String이고, String에는 length() 메서드가 있다. 따라서 이 코드는 컴파일된다.

하지만 실행 시점에 text가 가리키는 실제 객체는 없다. 그 상태에서 메서드를 호출하려고 해서 예외가 발생한다.

null을 저장하는 것 자체는 문제가 없고, null로 객체의 메서드를 호출하려는 순간 문제가 생긴다.

5단계. Unchecked Exception도 catch로 처리

Unchecked Exception도 잡을 수 있는지 확인해봤다.

public static void main(String[] args) {
    System.out.println("1. 실행 시작");

    String text = null;

    try {
        System.out.println(text.length());
    } catch (NullPointerException e) {
        System.out.println("2. 예외 처리: 실제 객체가 없습니다.");
    }

    System.out.println("3. 실행 끝");
}

실행 결과:

1. 실행 시작
2. 예외 처리: 실제 객체가 없습니다.
3. 실행 끝

Checked Exception을 잡았을 때처럼 마지막 줄까지 실행된다.

여기서 Unchecked는 처리할 수 없는 예외도, 처리할 필요가 없는 예외도 아니라는 것을 확인할 수 있었다. 컴파일러가 처리를 강제하지 않을 뿐이다.

6단계. 예외가 발생하기 전에 예방

앞에서는 동작을 비교하려고 NullPointerException을 잡았다. 하지만 이 상황에서는 사용하기 전에 null인지 확인할 수도 있다.

public static void main(String[] args) {
    String text = null;

    if (text != null) {
        System.out.println(text.length());
    } else {
        System.out.println("텍스트가 없어 길이를 구하지 않습니다.");
    }

    System.out.println("실행 끝");
}

실행 결과:

텍스트가 없어 길이를 구하지 않습니다.
실행 끝

이번에는 예외를 잡은 것이 아니라, null로 메서드를 호출하지 않아서 예외 발생을 예방했다.

처음 생각했던 “Unchecked Exception은 미리 대응할 수 없다”는 설명이 틀렸다는 것도 이 코드로 이해됐다.

실습 결과 비교

경우컴파일실행 결과
Checked, 처리 없음실패실행 단계로 넘어가지 못함
Checked, catch 사용성공예외를 잡고 이후 코드 실행
Checked, throws만 사용성공예외가 전달되고 main 스레드 종료
Unchecked, 처리 없음성공실행 중 예외가 발생해 main 스레드 종료
Unchecked, catch 사용성공예외를 잡고 이후 코드 실행
null 확인 후 사용성공예외 발생을 예방하고 정상 진행

예외 클래스의 상속 관계로 보면 다음과 같다.

Throwable
├── Error                         ← Unchecked
│   └── OutOfMemoryError
│
└── Exception
    ├── IOException               ← Checked
    ├── SQLException              ← Checked
    │
    └── RuntimeException          ← Unchecked
        ├── NullPointerException
        └── IllegalArgumentException

IOException은 Exception 계열이며 RuntimeException의 하위 클래스가 아니므로 Checked Exception이다.

NullPointerException은 RuntimeException 계열이라 Unchecked Exception이다. 엄밀히는 Error 계열도 Unchecked에 포함된다.

결국 예외가 어느 쪽에 속하는지는 상속 관계로 결정된다. “복구할 수 있으면 Checked, 복구할 수 없으면 Unchecked”라고 단정해서 외우면 안 된다.

느낀 점

처음에는 Checked와 Unchecked를 예외가 발생하는 시점이나 미리 대응할 수 있는지로 구분하려고 했다.

직접 비교해보니 차이가 가장 잘 보인 것은 1단계와 4단계였다. 둘 다 예외를 처리하지 않았지만 1단계는 실행 자체를 시작하지 못했고 4단계는 실행하다가 멈췄다.

또 throws를 붙이면 컴파일은 되지만 실행 중에는 여전히 예외로 종료되는 것을 보면서 예외를 전달하는 것과 잡아서 처리하는 것도 다르다는 것을 알게 됐다.

이제는 Checked와 Unchecked를 볼 때 “컴파일러가 예외를 잡거나 전달한다고 선언하도록 강제하는가?”를 기준으로 생각하면 될 것 같다.

0개의 댓글