J-S-14 자바 예외 처리
글 정보
- 카테고리
- Programming/Java/Starter
- 태그
- JavaLevel1
1. 에러와 예외의 정의
오류(Error)와 예외(Exception)는 프로그램 실행을 막는 상황을 뜻합니다.
정상적인 연산이나 실행이 불가능한 상태를 말합니다.
보통 아래 세 가지 시점에 발생합니다.
- 컴파일 타임: 문법적 오류
- 링크 타임: 리소스 연결 오류
- 런타임: 실행 중 발생하는 오류
예외 처리(Exception Handling)는 이러한 비정상적인 상황을 대비하는 기법입니다.
예외 상황을 별도로 처리함으로써 프로그램이 강제 종료되지 않고, 흐름을 계속 이어갈 수 있게(Fail-safe) 만드는 것이 핵심입니다.
자바에서는 java.lang.Exception 클래스를 기반으로 이를 관리합니다.
성공하는 상황(Happy Path)을 가정하고 코드를 작성하되, 예외 처리를 따로 분리하면 코드의 가독성이 크게 높아집니다.
(참고) Short Circuit의 원리란?
'Short Circuit'은 논리 연산(&&,||) 외에도 예외 처리 흐름에서 사용되는 개념입니다.
- 예외가 발생하는 즉시 나머지 로직 실행을 중단(단락)하고,
- 바로catch블록으로 점프하는 동작 방식을 의미합니다.
- 불필요한 연산을 막고 즉시 대응 체계로 넘어가는 효율적인 구조입니다.
2. 자바의 예외 계층 구조 (Error vs Exception)
자바는 발생 원인과 대응 가능성에 따라 크게 두 가지로 구분합니다. 이 둘을 나누는 기준은 "애플리케이션단에서 복구할 수 있는가?"입니다.
Error (에러)
시스템 레벨에서 발생하는 심각한 문제입니다.
- 특징:
- 개발자가 코드 수준에서 미리 예측하거나 방지하기 어렵습니다.
- 발생 시 대응이 불가능하거나, 복구 방법이 없는 경우가 대다수입니다.
- 예시:
OutOfMemoryError: 메모리 부족StackOverflowError: 스택 메모리 초과
Exception (예외)
개발자가 구현한 로직 내에서 발생하는 문제입니다.
- 특징:
try-catch등을 통해 적절히 대응하면 프로그램 종료를 막을 수 있습니다.- 연산 방향을 바꾸거나, 재시도 로직을 수행하는 등 복구가 가능합니다.
(Q&A) Error와 Exception을 굳이 나눈 이유는?
JVM 실행 환경 자체의 문제(Error)와 프로그램 논리의 문제(Exception)를 분리하여,
개발자가 "통제할 수 있는 영역"에만 집중하게 하기 위함입니다.
3. 두 가지 예외 종류 (Checked vs Unchecked)
대응 관점과 컴파일러의 개입 여부에 따라 나뉩니다.
Checked Exception
- 상속:
java.lang.Exception을 상속 (RuntimeException 제외) - 확인 시점: 컴파일(Compile) 타임
- 강제성:
- 반드시 예외 처리를 해야 합니다. (
try-catch또는throws) - 처리하지 않으면 컴파일 자체가 되지 않습니다.
- 특징:
- 실행 전 예측 가능한 예외들을 다룹니다.
- 최근에는 코드의 복잡도를 높인다는 논란(비판)이 있어, 실무에서는 신중히 사용하는 추세입니다.
Unchecked Exception
- 상속:
java.lang.RuntimeException을 상속 - 확인 시점: 런타임(Runtime)
- 강제성:
- 명시적인 예외 처리를 강제하지 않습니다.
- 특징:
- 개발자의 부주의나 논리 실수로 발생하는 경우가 많습니다.
4. 대표적인 예외 사례
런타임 예외 (Unchecked)
주로 개발자의 실수로 발생합니다.
- NullPointerException (NPE):
- 생성되지 않은(null) 객체를 참조하려 할 때 발생합니다.
String emptyBox = null;
// 내용물이 없는데(null) 길이를 재려고 해서 에러가 펑!
int length = emptyBox.length();
- ArrayIndexOutOfBoundsException:
- 배열의 크기를 벗어난 인덱스에 접근할 때 발생합니다.
- (실무 팁: 보통 로직 실수지만, 외부 데이터 파싱이나 Remote 통신 시 자주 발생할 수 있습니다.)
int[] numbers = {1, 2, 3}; // 인덱스는 0, 1, 2 까지 있음
// 5번째 칸은 존재하지 않으므로 에러 발생
System.out.println(numbers[5]);
- NumberFormatException:
- 숫자가 아닌 문자열을 숫자로 변환하려 할 때 발생합니다.
String text = "이천이십오";
// "이천이십오"는 아라비아 숫자가 아니라서 변환 실패
int year = Integer.parseInt(text);
- ClassCastException:
- 상속 관계가 아니거나 변환 불가능한 타입으로 형변환을 시도할 때 발생합니다.
Object box = "나는 문자열입니다";
// 문자열이 들어있는 박스를 갑자기 숫자(Integer)라고 우기면 에러 발생
Integer number = (Integer) box;
보편적 예외 (I/O 관련)
외부 시스템과의 연동 시 주로 발생하며, 통제하기 어려운 요소가 많습니다.
- File I/O:
- 파일이 존재하지 않거나, 디스크 용량이 부족할 때.
- 권한(Permission) 문제로 접근이 차단될 때.
// "secret.txt" 파일이 실제 경로에 없으면 에러 발생
FileInputStream file = new FileInputStream("C:/secret.txt");
- Network I/O:
- 호스트 수준에서 통제 불가능한 네트워크 단절.
- 보안 장치 이슈: IPS(침입 방지), WAF(웹 방화벽) 등에 의해 패킷이 차단되는 경우.
// 해당 IP나 포트(8080)가 닫혀있거나 방화벽에 막히면 연결 거부 에러 발생
Socket socket = new Socket("192.168.0.99", 8080);
- Database I/O:
- SQL 문법 오류 또는 DB 커넥션 끊김.
// 'members'라는 테이블이 없거나 컬럼명이 틀리면 에러 발생
stmt.executeQuery("SELECT * FROM 없는테이블 WHERE id = 'test'");
5. 예외 처리 방법 (Handling)
try - catch - finally
가장 기본적인 직접 처리 방식입니다.
- 원리:
- 특정 구간(Scope) 내의 오류를 감지하고 통합 처리합니다.
- 예외 발생 시 즉시 catch 블록으로 제어권(Control Flow)이 이동합니다.
- 장점:
- 비즈니스 로직과 예외 처리 코드를 분리하여 가독성을 높입니다.
try {
System.out.println("1. 연산을 시작합니다.");
int result = 10 / 0; // 여기서 에러 발생! (ArithmeticException)
System.out.println("2. 이 코드는 실행되지 않습니다."); // 실행 건너뜀
} catch (ArithmeticException e) {
// 에러가 발생하면 즉시 이곳으로 넘어옵니다.
System.out.println("3. 0으로 나눌 수 없어 에러를 잡았습니다.");
} finally {
// 에러 발생 여부와 상관없이 무조건 실행됩니다. (리소스 정리 등)
System.out.println("4. 계산기를 종료합니다.");
}
throws
예외 처리를 위임(Delegation)하는 방식입니다.
- 문법:
void method() throws Exception { ... } - 원리:
- 메서드 내부(Callee)에서 예외를 직접 처리하지 않습니다.
- 자신을 호출한 상위 메서드(Caller)에게 "이 예외가 발생할 수 있으니 네가 처리해"라고 던집니다.
- 특징:
- Checked Exception이 포함된 메서드를 호출하는 Caller는 반드시 예외 처리 코드를 작성해야 합니다(강제성 전파).
// 1. (Callee) 에러를 직접 처리하지 않고 throws로 던지는 메서드
public void findFile() throws FileNotFoundException {
// 파일이 없으면 에러가 나는데, 여기서 try-catch를 안 씀
FileReader fr = new FileReader("없는파일.txt");
}
// 2. (Caller) 위 메서드를 호출한 쪽에서 억지로라도 처리를 해야 함
public void mainLogic() {
try {
findFile(); // 여기서 에러가 넘어옴
} catch (FileNotFoundException e) {
System.out.println("넘겨받은 에러를 여기서 처리합니다.");
}
}