본문으로 건너뛰기
목록으로

Throwable vs Exception — 예외 계층 구조와 catch 대상 고르기

Johny Cho
Software Engineer @ Kurly

try-catch를 쓰다 보면 catch (Exception e)는 자주 보는데, 어떤 코드에서는 catch (Throwable t)가 등장합니다. 둘은 비슷해 보이지만 잡는 범위가 다르고, 잘못 고르면 잡지 말아야 할 문제까지 묻어 버립니다. 차이를 이해하는 핵심은 예외 계층 구조 — Throwable이 최상위이고, 그 아래 Error와 Exception이 성격이 전혀 다른 두 부류로 나뉜다는 점입니다.

계층 구조 한눈에​

Java에서 throw로 던지고 catch로 잡을 수 있는 모든 것은 Throwable을 상속합니다. 그 아래로 크게 두 가지로 나뉩니다 — Error와 Exception.

PlantUML 코드
@startuml
skinparam shadowing false
skinparam defaultFontName sans-serif
skinparam classFontSize 13
hide empty members
hide circle

class Object
class Throwable #FFF3E0
class Error #FCE4EC
class Exception #E9F2E9
class "RuntimeException\n(unchecked)" as RE #EEF3FB
class "IOException\nSQLException\n…" as CHK #E9F2E9
class "NullPointerException\nIllegalArgumentException\n…" as UNC #EEF3FB
class "OutOfMemoryError\nStackOverflowError\n…" as ERR #FCE4EC

Object <|-- Throwable
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- RE
Exception <|-- CHK
RE <|-- UNC
Error <|-- ERR
@enduml

PlantUML Java 예외 계층 구조

정리하면 이렇습니다.

  • Throwable — 던지고 잡을 수 있는 모든 것의 최상위 클래스. Error와 Exception의 공통 부모.
  • Error — JVM 수준의 심각한 문제. 어플리케이션이 복구하기 어렵습니다.
  • Exception — 어플리케이션이 다룰 수 있는 문제. 다시 checked와 unchecked(RuntimeException)로 나뉩니다.

Throwable — 던지고 잡는 모든 것의 최상위​

Throwable은 "예외"라는 개념 자체를 대표하는 클래스입니다. throw 문에 올 수 있고 catch로 받을 수 있는 건 오직 Throwable과 그 하위 타입뿐입니다 — 예컨대 throw "문자열"은 컴파일되지 않습니다.

예외를 다룰 때 쓰는 메서드도 전부 이 클래스에 정의돼 있습니다.

public class Throwable {
public String getMessage(); // 예외 메시지
public Throwable getCause(); // 원인 예외 (예외 체이닝)
public StackTraceElement[] getStackTrace(); // 스택 트레이스
public void printStackTrace();
// ...
}

즉 getMessage()·getCause()·스택 트레이스 같은 기능은 Error든 Exception이든 모두 공통으로 가집니다. 둘의 차이는 기능이 아니라 "어플리케이션이 이걸 잡아서 처리해야 하는가" 라는 용도에 있습니다.

Error — 잡지 말아야 할 심각한 문제​

Error는 JVM이나 시스템 수준에서 발생하는, 어플리케이션이 복구하기 어려운 심각한 상황을 나타냅니다. 대표적으로 OutOfMemoryError(힙 고갈), StackOverflowError(무한 재귀로 스택 초과), NoClassDefFoundError가 있습니다.

Error는 잡아서 처리하라고 만든 게 아닙니다. 메모리가 바닥난 상황을 catch 한들 어플리케이션이 할 수 있는 일은 거의 없고, 억지로 무시하면 오히려 비정상 상태로 계속 동작하다가 더 큰 문제를 만듭니다. 그래서 Error는 보통 그대로 전파시켜 JVM이 스레드를 종료하도록 둡니다.

void recurse(int n) {
recurse(n + 1); // 끝없는 재귀 → 결국 StackOverflowError
}

이런 StackOverflowError를 catch로 막는다고 근본 원인(잘못된 재귀)이 사라지지는 않습니다. Error가 보이면 잡을 게 아니라 코드를 고쳐야 한다는 신호로 읽는 편이 맞습니다.

Exception — 어플리케이션이 다루는 문제​

Exception은 어플리케이션 로직에서 충분히 일어날 수 있고, 잡아서 대응할 가치가 있는 문제입니다. 파일이 없거나, 네트워크가 끊기거나, 입력값이 잘못된 상황이 여기 속합니다. 그리고 Exception은 다시 두 가지로 나뉩니다.

1. checked 예외​

Exception을 상속하되 RuntimeException은 아닌 것들(IOException, SQLException 등)입니다. 컴파일러가 반드시 try-catch로 잡거나 throws로 전파하도록 강제합니다. 처리하지 않으면 컴파일 자체가 안 됩니다.

void read() throws IOException {   // throws로 전파를 명시하거나
Files.readAllLines(path);
}

void read2() {
try {
Files.readAllLines(path); // 또는 여기서 직접 잡아야 컴파일된다
} catch (IOException e) {
// 복구 로직
}
}

2. unchecked 예외​

RuntimeException과 그 하위 타입(NullPointerException, IllegalArgumentException, IndexOutOfBoundsException 등)입니다. 컴파일러가 처리를 강제하지 않습니다. 주로 프로그래밍 실수로 발생하므로, 일일이 잡기보다 코드를 고쳐 애초에 안 나게 하는 쪽이 맞습니다.

checked와 unchecked를 가르는 기준은 상속 관계 하나뿐입니다 — RuntimeException을 상속하면 unchecked, Exception만 상속하면 checked. Error도 unchecked로 분류됩니다(컴파일러가 처리를 강제하지 않는다는 의미에서).

catch (Throwable) vs catch (Exception)​

이제 처음 질문으로 돌아오면, 둘의 차이는 잡는 범위입니다.

  • catch (Exception e) — Exception과 그 하위(checked + unchecked)만 잡습니다. Error는 그대로 전파됩니다.
  • catch (Throwable t) — Exception뿐 아니라 Error까지 전부 잡습니다.
try {
doSomething();
} catch (Throwable t) { // OutOfMemoryError·StackOverflowError까지 무시한다
log.error("무언가 실패", t);
}

이 코드는 OutOfMemoryError처럼 그냥 죽는 편이 나은 상황까지 붙잡아 어플리케이션을 비정상 상태로 연명시킵니다. 그래서 일반적인 예외 처리에서는 catch (Throwable)을 쓰지 않고 catch (Exception)을 씁니다.

Throwable을 잡아야 하는 경우는 많지 않습니다 — 스레드 풀·프레임워크처럼 작업 하나가 어떤 이유로든 실패해도 전체가 멈추면 안 되는 최상위 경계에서, 로그를 남긴 뒤 다시 던지는(rethrow) 식으로 제한적으로 씁니다. 그마저도 보통은 Exception으로 충분합니다.

한 줄 정리​

구분위치checked?잡아야 하나
Throwable최상위—(직접 잡지 않음)
ErrorThrowable 바로 아래unchecked❌ 전파시킨다
Exception (checked)Throwable 아래✅ 강제✅ 잡거나 throws
RuntimeExceptionException 아래unchecked△ 코드로 예방

핵심만 추리면 이렇습니다. Throwable은 최상위, Error는 잡지 않고 전파할 심각한 문제, Exception은 잡아서 다룰 문제 — 그리고 평소 예외 처리는 catch (Exception)이 기본입니다.