Java 예외 처리 | try-catch, throws, 커스텀 예외
이 글의 핵심
Java 예외 처리에 대한 실전 가이드입니다. try-catch, throws, 커스텀 예외 등을 예제와 함께 상세히 설명합니다.
들어가며
Java의 예외 처리는 실패를 타입으로 표현하는 쪽에 가깝습니다. Checked Exception은 컴파일러가 try/throws 누락을 잡아 주는 특징이 있습니다. 이 글에서는 예외 계층, try–catch, throws, 커스텀 예외, try-with-resources까지 실무 패턴으로 정리합니다.
왜 중요한가?
- 컴파일 타임 안전성: Checked Exception으로 예외 처리 누락 방지
- 리소스 관리: try-with-resources로 메모리 누수 예방
- 디버깅: 스택 트레이스로 오류 원인 추적
- API 설계: 예외를 통한 명확한 에러 시그널링
1. 예외 계층 구조
Throwable
├── Error (시스템 오류, 처리 불가)
│ ├── OutOfMemoryError
│ └── StackOverflowError
└── Exception (처리 가능한 예외)
├── IOException (Checked)
├── SQLException (Checked)
└── RuntimeException (Unchecked)
├── NullPointerException
├── ArithmeticException
└── IndexOutOfBoundsException
2. try-catch-finally
기본 예외 처리
public class ExceptionExample {
public static void main(String[] args) {
try {
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("에러: " + e.getMessage());
} finally {
System.out.println("항상 실행");
}
}
}
여러 예외 처리
try {
String str = null;
System.out.println(str.length());
int[] arr = {1, 2, 3};
System.out.println(arr[10]);
} catch (NullPointerException e) {
System.out.println("Null 참조");
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("배열 범위 초과");
} catch (Exception e) {
System.out.println("기타 예외: " + e.getMessage());
}
다중 예외 처리 (Java 7+)
try {
// 코드
} catch (IOException | SQLException e) {
System.out.println("I/O 또는 DB 에러: " + e.getMessage());
}
3. throws - 예외 전파
기본 throws
import java.io.*;
public class FileHandler {
public void readFile(String path) throws IOException {
FileReader fr = new FileReader(path);
BufferedReader br = new BufferedReader(fr);
String line = br.readLine();
br.close();
}
public static void main(String[] args) {
FileHandler handler = new FileHandler();
try {
handler.readFile("file.txt");
} catch (IOException e) {
System.out.println("파일 읽기 실패: " + e.getMessage());
}
}
}
4. 커스텀 예외
Checked 예외
class InvalidAgeException extends Exception {
public InvalidAgeException(String message) {
super(message);
}
}
public class User {
private int age;
public void setAge(int age) throws InvalidAgeException {
if (age < 0 || age > 150) {
throw new InvalidAgeException("나이는 0~150 사이여야 합니다");
}
this.age = age;
}
}
Unchecked 예외
class InvalidEmailException extends RuntimeException {
public InvalidEmailException(String message) {
super(message);
}
}
public class User {
private String email;
public void setEmail(String email) {
if (!email.contains("@")) {
throw new InvalidEmailException("유효하지 않은 이메일");
}
this.email = email;
}
}
5. try-with-resources
import java.io.*;
// 기존 방식
public void readFile1(String path) {
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader(path));
String line = br.readLine();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
// try-with-resources (Java 7+)
public void readFile2(String path) {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
String line = br.readLine();
} catch (IOException e) {
e.printStackTrace();
}
}
6. 실전 예제
예제: 사용자 검증
class ValidationException extends Exception {
public ValidationException(String message) {
super(message);
}
}
class User {
private String name;
private int age;
private String email;
public void validate() throws ValidationException {
if (name == null || name.isEmpty()) {
throw new ValidationException("이름은 필수입니다");
}
if (age < 0 || age > 150) {
throw new ValidationException("나이는 0~150 사이여야 합니다");
}
if (email == null || !email.contains("@")) {
throw new ValidationException("유효하지 않은 이메일");
}
}
}
public class Main {
public static void main(String[] args) {
User user = new User();
user.setName("");
user.setAge(25);
user.setEmail("invalid");
try {
user.validate();
System.out.println("검증 성공");
} catch (ValidationException e) {
System.out.println("검증 실패: " + e.getMessage());
}
}
}
7. 예외 처리 베스트 프랙티스
1) 구체적인 예외를 먼저 catch
try {
// 코드
} catch (FileNotFoundException e) {
// 파일이 없을 때
} catch (IOException e) {
// 기타 I/O 오류
} catch (Exception e) {
// 최후의 안전망
}
주의: 부모 예외(Exception)를 먼저 catch하면 하위 예외가 도달하지 않습니다.
2) 예외를 무시하지 말 것
// ❌ 나쁜 예
try {
riskyOperation();
} catch (Exception e) {
// 아무것도 안 함
}
// ✅ 좋은 예
try {
riskyOperation();
} catch (Exception e) {
logger.error("Operation failed", e);
throw new RuntimeException("Failed to process", e);
}
3) 예외 체이닝 활용
public void processData(String data) throws DataProcessingException {
try {
// 복잡한 처리
parseJson(data);
} catch (JsonParseException e) {
// 원본 예외를 cause로 포함
throw new DataProcessingException("Invalid data format", e);
}
}
4) 리소스는 try-with-resources 사용
// ✅ 자동으로 close() 호출됨
try (FileInputStream fis = new FileInputStream("file.txt");
BufferedInputStream bis = new BufferedInputStream(fis)) {
// 파일 읽기
} catch (IOException e) {
// 예외 처리
}
// close()가 자동 호출됨 (역순으로)
5) Checked vs Unchecked 선택 기준
| 상황 | 선택 | 이유 |
|---|---|---|
| 복구 가능한 오류 (파일 없음, 네트워크 끊김) | Checked (extends Exception) | 호출자가 반드시 처리하도록 강제 |
| 프로그래밍 오류 (null 참조, 배열 범위 초과) | Unchecked (extends RuntimeException) | 코드 수정으로 해결해야 함 |
| 비즈니스 로직 위반 | 상황에 따라 | 복구 가능하면 Checked, 아니면 Unchecked |
8. 실전 패턴
패턴 1: 예외 변환 (Exception Translation)
public class UserService {
private UserRepository repository;
public User findUser(Long id) throws UserNotFoundException {
try {
return repository.findById(id);
} catch (SQLException e) {
// DB 예외를 도메인 예외로 변환
throw new UserNotFoundException("User not found: " + id, e);
}
}
}
패턴 2: 예외 집계 (Exception Aggregation)
public class BatchProcessor {
public void processBatch(List<Item> items) throws BatchProcessingException {
List<Exception> errors = new ArrayList<>();
for (Item item : items) {
try {
processItem(item);
} catch (Exception e) {
errors.add(e);
}
}
if (!errors.isEmpty()) {
throw new BatchProcessingException("일부 항목 처리 실패", errors);
}
}
}
패턴 3: Retry 패턴
public <T> T retryOperation(Supplier<T> operation, int maxRetries)
throws Exception {
Exception lastException = null;
for (int i = 0; i < maxRetries; i++) {
try {
return operation.get();
} catch (TransientException e) {
lastException = e;
Thread.sleep(1000 * (i + 1)); // 지수 백오프
}
}
throw new RuntimeException("Max retries exceeded", lastException);
}
9. 흔한 실수와 해결
실수 1: 예외를 흐름 제어로 사용
// ❌ 나쁜 예 - 예외를 일반 로직으로 사용
try {
return map.get(key);
} catch (NullPointerException e) {
return defaultValue;
}
// ✅ 좋은 예
return map.getOrDefault(key, defaultValue);
실수 2: 너무 넓은 catch
// ❌ 나쁜 예
try {
complexOperation();
} catch (Exception e) {
// 모든 예외를 동일하게 처리
}
// ✅ 좋은 예
try {
complexOperation();
} catch (IOException e) {
// I/O 오류 처리
} catch (ValidationException e) {
// 검증 오류 처리
}
실수 3: finally에서 return
// ❌ 나쁜 예 - finally의 return이 try의 return을 덮어씀
public int getValue() {
try {
return 1;
} finally {
return 2; // 항상 2가 반환됨
}
}
10. 성능 고려사항
예외는 비용이 크다
// 스택 트레이스 생성 비용
long start = System.nanoTime();
for (int i = 0; i < 10000; i++) {
try {
throw new Exception();
} catch (Exception e) {
// 처리
}
}
long elapsed = System.nanoTime() - start;
// 일반 제어 흐름보다 100~1000배 느림
최적화 팁:
- 정상 흐름에서는 예외를 사용하지 말 것
- 고빈도 경로에서는 예외 대신 Optional이나 Result 타입 고려
- 스택 트레이스가 필요 없으면
fillInStackTrace()오버라이드
정리
핵심 요약
- Checked vs Unchecked: 복구 가능성으로 판단
- try-catch-finally: 예외 처리와 리소스 정리
- try-with-resources: AutoCloseable 리소스 자동 관리
- 예외 체이닝: 원본 예외를 cause로 보존
- 베스트 프랙티스: 구체적 catch, 예외 무시 금지, 흐름 제어 금지
실무 체크리스트
- Checked Exception은 복구 가능한 경우만 사용
- 예외 메시지에 충분한 컨텍스트 포함
- 원본 예외를 cause로 전달
- 리소스는 try-with-resources 사용
- 로깅과 예외 던지기를 동시에 하지 말 것
다음 단계
- Java I/O
- Java 멀티스레딩
- Java Spring Boot
관련 글
- C++ 예외 처리 | try/catch/throw
- C++ 예외 처리 | try-catch-throw와 예외 vs 에러 코드, 언제 뭘 쓸지
- JavaScript 에러 처리 | try-catch, Error 객체, 커스텀 에러
- Python 예외 처리 | try-except, raise, 커스텀 예외 완벽 정리
- C++ Exception Performance |