Java 예외 처리 - try-catch, 예외 클래스, 그리고 흔한 실수들
1. 예외(Exception)란?
프로그램 실행 중 발생하는 예상치 못한 상황을 예외(Exception)라고 한다. 자바는 예외 클래스를 미리 만들어두어서, 사용자가 적절한 예외 케이스를 가져다 쓰면 된다.
예외의 종류
Throwable
├── Error (시스템 에러, 복구 불가)
│ ├── OutOfMemoryError
│ └── StackOverflowError
│
└── Exception (프로그램 에러, 복구 가능)
├── RuntimeException (Unchecked Exception)
│ ├── NullPointerException
│ ├── ArrayIndexOutOfBoundsException
│ ├── ArithmeticException
│ └── IllegalArgumentException
│
└── 기타 Exception (Checked Exception)
├── IOException
├── SQLException
└── FileNotFoundException
- Checked Exception: 컴파일 시점에 처리를 강제함 (try-catch 또는 throws 필수)
- Unchecked Exception: 런타임에 발생, 처리 선택적
2. try-catch-finally
예외를 처리하는 기본 구문이다.
public class ExceptionBasic {
public static void main(String[] args) {
try {
// 예외가 발생할 수 있는 코드
int result = 10 / 0;
System.out.println(result);
} catch (ArithmeticException e) {
// 예외 처리 코드
System.out.println("0으로 나눌 수 없습니다: " + e.getMessage());
} finally {
// 예외 발생 여부와 관계없이 항상 실행
System.out.println("연산 종료");
}
}
}
여러 예외 처리하기
public class MultiCatch {
public static void main(String[] args) {
try {
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // ArrayIndexOutOfBoundsException
String str = null;
System.out.println(str.length()); // NullPointerException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("배열 인덱스 초과: " + e.getMessage());
} catch (NullPointerException e) {
System.out.println("null 참조 에러: " + e.getMessage());
} catch (Exception e) {
// 위에서 잡지 못한 모든 예외
System.out.println("기타 예외: " + e.getMessage());
}
}
}
멀티 catch (Java 7+)
try {
// 예외 발생 가능 코드
} catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
// 두 예외를 동일하게 처리
System.out.println("예외 발생: " + e.getMessage());
}
3. try-with-resources
Java 7부터 도입된 문법으로, AutoCloseable을 구현한 리소스를 자동으로 닫아준다. 파일, 데이터베이스 연결 등의 컨텍스트 매니징을 자동으로 해주는 역할이다.
// 기존 방식 (직접 close 호출)
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close(); // 닫아줘야 함
} catch (IOException e) {
e.printStackTrace();
}
}
}
// try-with-resources 방식 (자동 close)
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// br.close()가 자동으로 호출됨!
여러 리소스 사용하기
try (
FileInputStream fis = new FileInputStream("input.txt");
FileOutputStream fos = new FileOutputStream("output.txt")
) {
int data;
while ((data = fis.read()) != -1) {
fos.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
4. 계산기 예제와 흔한 실수들
계산기를 만들면서 자주 겪는 문제들을 살펴보자.
문제 1: switch문의 break 누락
public class Calculator {
public static int calculate(int a, int b, char op) {
int result = 0;
switch (op) {
case '+':
result = a + b;
// break가 없으면 아래 case들이 계속 실행됨!
case '-':
result = a - b;
case '*':
result = a * b;
case '/':
result = a / b;
}
return result;
}
}
매번 break를 써줘야 하는 것이 boilerplate처럼 느껴질 수 있다. Java 14부터는 switch expression을 사용하면 break 없이 깔끔하게 작성할 수 있다.
// Java 14+ switch expression
public static int calculate(int a, int b, char op) {
return switch (op) {
case '+' -> a + b;
case '-' -> a - b;
case '*' -> a * b;
case '/' -> a / b;
default -> throw new IllegalArgumentException("Unknown operator: " + op);
};
}
문제 2: 정수 나눗셈 문제
int a = 5;
int b = 2;
float result = a / b;
System.out.println(result); // 2.0 (기대한 2.5가 아님!)
왜 이런 결과가 나올까?
자바에서 산술 연산의 타입은 피연산자의 타입에 의해서 결정된다.
피연산자 타입 조합에 따른 결과:
- (정수, 정수) → 정수
- (실수, 정수) → 실수
- (실수, 실수) → 실수
a / b를 계산할 때:
- 먼저 두 정수를 계산 → 결과는 정수
2 - 이 정수 값을
result변수에 할당 →2.0
result가 float인지 int인지는 할당 시점에 결정되므로, 이미 연산이 끝난 후에는 영향을 주지 않는다.
해결 방법
// 방법 1: 피연산자 중 하나를 실수로 캐스팅
float result1 = (float) a / b; // 2.5
// 방법 2: 피연산자 중 하나를 실수 리터럴로
float result2 = a / 2.0f; // 2.5
// 방법 3: 1.0을 곱해서 실수로 변환
float result3 = 1.0f * a / b; // 2.5
5. 예외 던지기 (throw, throws)
throw: 예외 직접 발생시키기
public class Calculator {
public static int divide(int a, int b) {
if (b == 0) {
throw new ArithmeticException("0으로 나눌 수 없습니다");
}
return a / b;
}
}
throws: 예외 처리 위임하기
public class FileProcessor {
// 이 메서드를 호출하는 쪽에서 IOException을 처리해야 함
public void readFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
String line = br.readLine();
br.close();
}
}
6. 사용자 정의 예외
특정 상황에 맞는 예외 클래스를 직접 만들 수 있다.
// 사용자 정의 예외 클래스
class InsufficientBalanceException extends Exception {
private int balance;
private int withdrawAmount;
public InsufficientBalanceException(int balance, int withdrawAmount) {
super("잔액이 부족합니다. 현재 잔액: " + balance + ", 출금 요청: " + withdrawAmount);
this.balance = balance;
this.withdrawAmount = withdrawAmount;
}
public int getBalance() {
return balance;
}
public int getWithdrawAmount() {
return withdrawAmount;
}
}
// 사용 예시
class BankAccount {
private int balance;
public BankAccount(int balance) {
this.balance = balance;
}
public void withdraw(int amount) throws InsufficientBalanceException {
if (amount > balance) {
throw new InsufficientBalanceException(balance, amount);
}
balance -= amount;
}
}
정리
| 구문 | 설명 |
|---|---|
try-catch |
예외 발생 가능 코드를 감싸고 예외 처리 |
finally |
예외 발생 여부와 관계없이 항상 실행 |
try-with-resources |
AutoCloseable 리소스 자동 해제 |
throw |
예외를 직접 발생시킴 |
throws |
메서드에서 예외 처리를 호출자에게 위임 |
주의할 점
- switch문에서 break 잊지 말기 (또는 Java 14+ switch expression 사용)
- 정수 나눗셈 결과는 정수 - 실수 결과가 필요하면 캐스팅 필요
- try-with-resources로 리소스 누수 방지 - 파일, DB 연결 등에 활용