예외처리
예외처리
왜 쓰는지
프로그램 실행 중 예상 밖의 상황이 발생할 수 있습니다. 예외처리 없이는 프로그램이 강제로 종료되어 시스템 장애로 이어질 수 있습니다.
핵심: 예외처리는 프로그램의 안정성과 신뢰성을 보장하는 필수 메커니즘입니다.
예외 계층 구조
| 계층 | 설명 | 특징 | 예시 |
|---|---|---|---|
Throwable |
모든 예외와 에러의 최상위 클래스 | JVM이 던질 수 있는 모든 문제의 부모 | - |
Error |
시스템 레벨 오류 | 애플리케이션에서 복구 불가능 | OutOfMemoryError, StackOverflowError |
Exception |
애플리케이션에서 처리 가능한 예외 | 대부분의 예외가 여기 포함 | - |
RuntimeException |
Exception의 자식 |
대표적인 언체크 예외 | NullPointerException, IllegalArgumentException |
핵심 정리 - Error → 복구 불가능, 잡지 않는 것이 원칙 - Exception → 애플리케이션 레벨 예외 - RuntimeException → 개발자 실수성 예외
체크 예외 vs 언체크 예외
어떤 차이가 있는가?
| 구분 | 체크 예외 (Checked) | 언체크 예외 (Unchecked) |
|---|---|---|
| 기준 | Exception 상속 (단 RuntimeException 제외) |
RuntimeException 상속 |
| 컴파일 검사 | ✅ 컴파일러가 검사 | ❌ 검사하지 않음 |
| 처리 의무 | ✅ 반드시 catch 또는 throws |
❌ 선택 |
| 안정성 | ↑ 높음 | ↓ 낮음 |
| 편의성 | ↓ 낮음 | ↑ 높음 |
| 대표 예외 | SQLException, IOException, FileNotFoundException |
NullPointerException, IllegalArgumentException, IndexOutOfBoundsException |
언제 각각을 사용하는가?
체크 예외를 쓰는 경우: - 외부 시스템과의 상호작용: DB, 파일 I/O, 네트워크 통신 - 복구 가능성이 있는 상황: 파일이 없으면 생성, 연결 실패 시 재시도
언체크 예외를 쓰는 경우: - 개발자의 논리적 실수: null 체크 미흡, 범위 검증 실패 - 복구가 불가능한 상황: 조건이 만족되면 절대 발생하지 않아야 함
장점과 단점
체크 예외
장점 - 컴파일러가 강제하므로 예외 처리 빠뜨림 방지 - 개발자에게 예외 발생 가능성을 명확히 알림
단점 - 모든 예외를 명시적으로 선언해야 함 (boilerplate 증가) - 깊은 콜 스택에서 매번 throws 전파해야 함 - 과도한 catch-rethrow 패턴 발생
언체크 예외
장점 - 메서드 시그니처가 깔끔함 - 선택적 예외 처리 가능 - 불필요한 throws 체인 제거
단점 - 컴파일러가 검사하지 않아 예외 빠뜨리기 쉬움 - 어떤 예외가 발생할 수 있는지 예측하기 어려움 - 문서화가 중요함 (하지만 누락되기 쉬움)
요약
Exception
├─ Checked Exception → 외부 시스템과의 상호작용 (DB, 파일, 네트워크)
└─ RuntimeException (Unchecked Exception) → 개발자 논리 오류
예외 처리 방법
어떻게 처리하는가?
| 방법 | 설명 | 사용 시점 |
|---|---|---|
| catch | 예외를 잡아서 직접 처리 | 현재 메서드에서 복구 가능 |
| throws | 호출한 곳으로 예외 전달 | 상위 계층에서 처리하도록 위임 |
코드 예시
// catch: 현재 메서드에서 처리
try {
readFile("data.txt");
} catch (IOException e) {
System.out.println("파일을 찾을 수 없습니다");
}
// throws: 상위 계층으로 전달
public void readFile(String filename) throws IOException {
new FileInputStream(filename);
}
부모 예외의 다형성
자식 예외는 부모 예외 타입으로도 catch할 수 있습니다.
try {
// NullPointerException, IllegalArgumentException 등 다양한 예외 발생 가능
} catch (Exception e) { // 모든 Exception 하위 예외를 catch
System.out.println("예외 발생: " + e.getMessage());
}
주의: catch (Exception e)는 편하지만, 예외가 무엇인지 구분하지 않으므로 특정 예외만 catch하는 것이 좋습니다.
try-catch-finally 구조
어떻게 구조화되어 있는가?
| 블록 | 역할 | 실행 조건 |
|---|---|---|
| try | 정상 로직 수행 | 항상 실행 |
| catch | 예외 발생 시 처리 | 예외 발생 시만 실행 |
| finally | 리소스 정리 및 마무리 코드 | 항상 실행 (return 직전) |
실행 흐름
코드 예시
try {
// 정상 흐름
connection.createStatement().execute("SELECT ...");
System.out.println("쿼리 실행 완료");
} catch (SQLException e) {
// 예외 처리
System.out.println("쿼리 실행 오류: " + e.getMessage());
} finally {
// 항상 실행 - 리소스 정리
connection.close();
System.out.println("연결 종료");
}
언제 쓰는가?
- try: 예외 가능성이 있는 코드
- catch: 해당 예외를 처리할 수 있는 로직
- finally: 반드시 정리해야 하는 리소스 (연결, 스트림 등)
try-with-resources
왜 필요한가?
finally로 리소스를 정리하는 것은 번거롭고 실수하기 쉽습니다. Java 7부터 도입된 자동 리소스 관리 방식입니다.
어떻게 쓰는가?
// finally를 사용한 기존 방식
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("file.txt"));
String line = reader.readLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// try-with-resources를 사용한 개선 방식 (권장)
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line = reader.readLine();
} catch (IOException e) {
e.printStackTrace();
}
// reader.close()가 자동으로 호출됨
장점 vs finally
기존 방식 (try-catch-finally)
- 리소스 정리 코드가 길어짐
- null 체크 필수
- 중첩된 try-catch로 복잡해짐
- close() 호출 빠뜨리기 쉬움
try-with-resources (권장)
- 자동으로 close() 호출
- 코드가 간결함
- 여러 리소스 한 번에 관리 가능
- null 안전 (리소스 누수 방지)
실행 순서 비교
기존 방식: try → (exception) → catch → finally → close()
try-with-resources: try → (exception) → catch → (자동 close)
사용 조건
AutoCloseable 인터페이스를 구현해야 합니다.
class MyResource implements AutoCloseable {
@Override
public void close() throws Exception {
System.out.println("리소스 정리");
// 실제 리소스 해제 로직
}
}
// 사용
try (MyResource resource = new MyResource()) {
// resource 사용
} // 자동으로 resource.close() 호출
여러 리소스 관리
try (
InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt")
) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
e.printStackTrace();
}
// input과 output이 자동으로 닫힘 (역순: output → input)
주의할 점
AutoCloseable이 아닌 리소스는 사용 불가
// ❌ 잘못된 예시
try (String str = "hello") { // String은 AutoCloseable을 구현하지 않음
}
// ✅ 올바른 예시
try (BufferedReader reader = new BufferedReader(...)) {
}
예외가 발생해도 close()는 실행됨 - try 블록 실행 중 예외 발생 → close() 실행 → catch 블록 실행 - 따라서 리소스 누수 걱정 없음
실전 가이드
언제 어떤 것을 사용할까?
| 상황 | 선택 | 이유 |
|---|---|---|
| DB 연결, 파일 I/O, 네트워크 | try-with-resources | 리소스를 안전하게 정리하기 위함 |
| 외부 시스템 호출 | catch로 체크 예외 처리 | 복구 가능한 상황에 대비 |
| 입력값 검증 실패 | throw new IllegalArgumentException() | 개발자 실수를 명확히 함 |
| NullPointerException 예방 | null 체크 코드 추가 | 예외보다 사전 검증 |
| 깊은 콜 스택 | 상위 계층으로 throws 위임 | boilerplate 감소 |
안티패턴 (피해야 할 패턴)
❌ 1. 예외를 무시하는 코드
✅ 올바른 방식:
❌ 2. 일반적인 Exception catch
✅ 올바른 방식:
❌ 3. finally에서 return (예외 숨김)
정리
| 구분 | 원칙 |
|---|---|
| Error | 절대 잡지 않는다 |
| Checked Exception | 외부 시스템 (DB, 파일, 네트워크) |
| RuntimeException | 개발자 로직 오류 |
| 리소스 관리 | try-with-resources 필수 (IO/DB) |
| 예외 처리 | 무시하지 말고, 구체적으로 처리하거나 의미 있게 전파 |