🧩 자바의 예외 계층 구조
자바의 예외(Exception)는 프로그램 실행 중 발생할 수 있는 오류를 나타내는 객체다.
자바의 모든 예외는 Throwable 클래스를 상속받으며, 이는 다시 Error와 Exception으로 나뉜다.
(✅: 체크 예외, ❌: 언체크 예외)
Object
|
Throwable
/ \
Error ❌ Exception ✅
/ \
✅ RuntimeException ❌
\
❌
- Error:
- OutOfMemoryError처럼 시스템에 심각한 문제가 발생하여 애플리케이션 수준에서 복구할 수 없는 오류다.
- 개발자는 Error를 잡으려고 해서는 안 된다.
- Exception:
- 애플리케이션 코드에서 발생할 수 있는 실질적인 예외의 최상위 클래스다.
자바는 이러한 예외를 '체크 예외(Checked Exception)'와 '언체크 예외(Unchecked Exception)'로 구분하여 처리하도록 강제한다.
이 둘의 가장 큰 차이점은 "컴파일 시점에 확인하느냐(체크) 그렇지 않으냐(언체크)"에 있다.
🧩 예외 처리의 기본 규칙
예외 처리는 폭탄 돌리기와 비슷하다. 잡아서 처리하거나, 처리할 수 없다면 밖으로 던져야 한다.
- 예외를 잡거나 던질 때 지정된 예외 타입뿐만 아니라 그 자식들도 함께 처리된다.
- 예를 들어, Exception을 catch로 잡으면, Exception의 자식 예외는 모두 잡을 수 있다.
- 또한, Exception을 throws로 던지면, Exception의 자식 예외는 모두 던질 수 있다.
만약 예외를 처리하지 못하고 계속 던지게 되면, 자바의 main 쓰레드의 경우 예외 로그를 남기면서 시스템이 종료된다.
🚨 하지만 웹 애플리케이션의 경우 여러 사용자의 요청을 처리하기 때문에 하나의 예외 때문에 전체 시스템이 종료되면 안 된다.
따라서 해당 예외를 받아서 오류 페이지를 보여주거나, API를 통해 오류 응답을 보내야 한다.
✅ 체크 예외 (Checked Exception)
👉 체크 예외는 컴파일러가 예외 처리 여부를 강제로 확인하는 예외다. 즉, 반드시 처리해야 하는 예외다.
⚠️ 만약 체크 예외를 던질 수 있는 메서드를 호출하면서, 예외를 처리하는 코드를 작성하지 않으면, 컴파일 오류가 발생하게 된다.
이러한 강제성은 개발자가 프로그램 실행 중에 발생할 수 있는 예외적인 상황을 미리 예측하고 대비하도록 유도하여, 프로그램의 안정성을 높이는 데 목적이 있다.
특징:
- Exception과, 그 하위 예외 중 RuntimeException을 상속하지 않는 예외
- 컴파일 시점에 예외 처리 여부를 확인
- try-catch를 통해 예외를 잡거나, throws를 통해 메서드 밖으로 예외를 던져서 호출한 곳에서 처리하도록 위임해야 함
- 주로 외부 요인에 의해 발생하며, 개발자가 코드로 복구할 수 있는 상황에 사용됨
예시:
- IOException: 입출력 작업 중 문제가 발생했을 때 던져지는 예외
- FileNotFoundException: 존재하지 않는 파일에 접근하려고 할 때 발생하는 예외
- SQLException: 데이터베이스 관련 작업 수행 중 오류가 발생했을 때 사용되는 예외
- ClassNotFoundException: 클래스를 동적으로 로드하려고 할 때 해당 클래스를 찾을 수 없는 경우 발생하는 예외
처리 방법:
// 1. try-catch로 직접 처리
try {
FileReader fr = new FileReader("file.txt");
} catch (FileNotFoundException e) {
log.error("예외 발생: {}", e.getMessage(), e);
}
// 2. throws로 호출한 곳에 처리 위임
public void readFile() throws FileNotFoundException {
FileReader fr = new FileReader("file.txt");
}
로그의 마지막 인자에 예외 객체를 넘기면 해당 예외의 스택 트레이스를 출력해 준다.
❌ 언체크 예외 (Unchecked Exception)
👉 언체크 예외는 컴파일러가 예외 처리 여부를 확인하지 않는 예외다. 즉, 반드시 처리할 필요는 없는 예외다.
따라서 언체크 예외는 try-catch나 throws를 명시적으로 사용하지 않아도 컴파일 오류가 발생하지 않는다.
특징:
- RuntimeException과 그 자식 예외
- 실행 시점(Runtime)에 발생함
- 컴파일러가 예외 처리를 강제하지 않으므로, 개발자가 필요에 따라 선택적으로 처리할 수 있음
- 주로 개발자의 실수나 논리적인 오류로 인해 발생
예시:
- NullPointerException: null 값을 갖는 참조 변수에 접근하려고 할 때 발생
- ArrayIndexOutOfBoundsException: 배열의 범위를 벗어난 인덱스에 접근하려고 할 때 발생
- ArithmeticException: 0으로 나누는 등 산술적으로 불가능한 연산을 수행할 때 발생
- ClassCastException: 잘못된 타입으로 객체를 형 변환하려고 할 때 발생
- IllegalArgumentException: 메서드에 잘못된 인자를 전달했을 때 발생
처리 방법:
- 언체크 예외 처리는 필수는 아니다.
- 하지만 발생 가능성을 예측할 수 있고, 해당 상황에서 특별한 처리가 필요하다면 try-catch로 처리할 수 있다.
- 근본적으로는 예외가 발생하지 않도록 코드를 수정하는 것이 더 바람직하다.
// 예외가 발생하지 않도록 미리 확인하는 것이 좋음
String text = null;
if (text != null) {
System.out.println(text.length()); // NPE 방지
}
// 필요에 따라 try-catch로 처리할 수도 있음
try {
int result = 10 / 0;
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없습니다.");
}
🧩 체크 예외의 장단점
체크 예외는 반드시 try-catch로 잡아서 처리하거나, 밖으로 던지는 throws를 선언해야 한다. 그렇지 않으면 컴파일 오류가 발생한다
이 때문에 다음과 같은 장점과 단점이 존재한다.
- 장점: 개발자가 실수로 예외를 누락하지 않도록 컴파일러가 알려주므로, 시스템의 안정성이 향상된다.
- 단점: 처리할 필요가 없거나 처리할 수 없는 예외도 모두 처리하거나 던져야 하며, 계층형 아키텍처에서 의존 관계 문제를 유발한다.
예를 들어, 리포지토리에서 특정 DB에 종속적인 체크 예외를 던진다면, 이를 호출하는 서비스 계층은 해당 기술에 종속되게 된다.
👉 따라서 최근 실무에서는 처리할 수 없거나, 처리할 필요 없는 예외는 "언체크 예외"로 설계하는 추세다.
- 서비스 코드에서 예외 복구 로직을 직접 가지는 것보다, 전역 예외 처리(@ControllerAdvice)로 관리하는 것이 더 낫다.
- Spring, JPA 같은 최신 프레임워크에서도 대부분 체크 예외를 "언체크 예외"로 감싸서 던진다.
🧩 언체크 예외의 장단점
언체크 예외는 예외를 잡아서 처리할 수 없을 때, 밖으로 던지는 throws를 생략할 수 있다.
이 때문에 다음과 같은 장점과 단점이 존재한다.
- 장점: 처리할 필요가 없는 예외를 무시해도 되며, 의존 관계 문제가 없다.
- 단점: 개발자가 실수로 예외를 누락할 수 있다.
👉 결국 실무 관점에서 체크 예외와 언체크 예외의 차이는 throws 선언의 필수 여부다.
📊 체크 예외 vs 언체크 예외 비교
| 구분 | ✅ 체크 예외 (Checked Exception) | ❌ 언체크 예외 (Unchecked Exception) |
| 확인 시점 | 컴파일 시점 (Compile-time) | 실행 시점 (Run-time) |
| 처리 강제성 | 필수 (try-catch 또는 throws) | 선택 (처리하지 않아도 컴파일은 됨) |
| 상속 관계 | Exception 클래스 상속 (단, RuntimeException 제외) | RuntimeException 클래스 상속 |
| 발생 원인 | 외부 요인 (파일, 네트워크 등), 복구 가능성 있음 | 프로그래머의 실수, 논리적 오류 |
| 대표 예시 | IOException, SQLException | NullPointerException, ArrayIndexOutOfBoundsException |
| 목표 | 안정적인 프로그램 설계 유도 | 개발자의 자율성에 맡김 |
📚 요약
- 체크 예외: RuntimeException을 상속하지 않는 모든 Exception의 자식 클래스
- 컴파일러가 예외 처리 여부를 강제로 확인(check)함
- 따라서 개발자는 반드시 try-catch로 예외를 잡아서 처리하거나, throws 키워드를 사용하여 메서드 밖으로 예외를 던져야 함
- 그렇지 않으면 컴파일 오류가 발생함
- 예상 가능한 문제거나 복구 가능성 있을 때 사용
- 언체크 예외: RuntimeException과 그 모든 자식 클래스
- 파일러가 예외 처리 여부를 확인하지 않음
- throws 선언 없이도 예외를 밖으로 던질 수 있으며, 잡지 않은 예외는 자동으로 호출한 쪽으로 계속 전파됨
- 프로그래밍 오류 또는 복구 불가능할 때 사용
- 실무에서는 체크 예외 최소화, 언체크 예외와 전역 예외 처리로 단순화
'Java' 카테고리의 다른 글
| [Java] 주요 자료구조 클래스의 핵심 메서드 (0) | 2025.10.08 |
|---|---|
| [Java] 진법 변환 (10진수 ↔ n진수) (0) | 2025.09.30 |
| [Java] 컬렉션 순회 중 발생하는 ConcurrentModificationException (0) | 2025.09.03 |
| [Java] >>(산술 시프트)와 >>>(논리 시프트)의 차이 (2의 보수와 오버플로우) (1) | 2025.06.23 |
| [Java] Arrays.binarySearch()와 Collections.binarySearch()의 차이 (2) | 2025.06.21 |