Day 14: 예외 처리
예외(Exception)는 프로그램 실행 중 발생하는 예기치 못한 상황입니다. 예외 처리를 통해 프로그램이 갑자기 종료되는 것을 방지하고, 적절한 복구 로직을 실행할 수 있습니다. Java는 예외를 객체로 다루며, 풍부한 예외 계층 구조를 제공합니다.
try-catch-finally 기본
예외를 잡고 처리하는 기본 구조입니다.
public class ExceptionBasic {
public static void main(String[] args) {
// 기본 try-catch
try {
int result = 10 / 0;
System.out.println("결과: " + result); // 실행되지 않음
} catch (ArithmeticException e) {
System.out.println("0으로 나눌 수 없습니다: " + e.getMessage());
}
// 여러 예외 처리
try {
String text = null;
// text.length(); // NullPointerException
int[] arr = {1, 2, 3};
System.out.println(arr[5]); // ArrayIndexOutOfBoundsException
} catch (NullPointerException e) {
System.out.println("null 참조 에러: " + e.getMessage());
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("배열 인덱스 초과: " + e.getMessage());
} catch (Exception e) {
System.out.println("기타 에러: " + e.getMessage());
} finally {
// 예외 발생 여부에 관계없이 항상 실행
System.out.println("정리 작업 실행 (finally)");
}
// 멀티 catch (Java 7+)
try {
String numStr = "abc";
int num = Integer.parseInt(numStr);
} catch (IllegalArgumentException e) {
System.out.println("변환 에러: " + e.getMessage());
}
System.out.println("프로그램 계속 실행됨");
}
}
try-with-resources (자동 리소스 해제)
AutoCloseable을 구현한 리소스를 자동으로 닫아줍니다.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TryWithResources {
// 커스텀 리소스
static class DatabaseConnection implements AutoCloseable {
String name;
DatabaseConnection(String name) {
this.name = name;
System.out.println(name + " 연결 열림");
}
void query(String sql) {
System.out.println(name + " 쿼리 실행: " + sql);
}
@Override
public void close() {
System.out.println(name + " 연결 닫힘");
}
}
public static void main(String[] args) {
// try-with-resources: 자동으로 close() 호출
try (DatabaseConnection db = new DatabaseConnection("MySQL")) {
db.query("SELECT * FROM users");
// db.close()가 자동으로 호출됨
}
// 여러 리소스 관리
try (
DatabaseConnection db1 = new DatabaseConnection("Primary");
DatabaseConnection db2 = new DatabaseConnection("Secondary")
) {
db1.query("INSERT INTO logs VALUES (...)");
db2.query("SELECT * FROM cache");
}
// 역순으로 close됨: db2 먼저, 그 다음 db1
System.out.println("모든 리소스가 정리됨");
}
}
Checked vs Unchecked 예외
Java 예외 계층과 두 종류의 예외 차이를 이해합니다.
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
public class CheckedUnchecked {
// Checked 예외: 컴파일러가 처리를 강제함
// IOException, SQLException, FileNotFoundException 등
static String readFile(String path) throws FileNotFoundException {
File file = new File(path);
if (!file.exists()) {
throw new FileNotFoundException("파일을 찾을 수 없습니다: " + path);
}
return "파일 내용";
}
// Unchecked 예외 (RuntimeException): 처리를 강제하지 않음
// NullPointerException, IllegalArgumentException 등
static int divide(int a, int b) {
if (b == 0) {
throw new IllegalArgumentException("나누는 수는 0이 될 수 없습니다.");
}
return a / b;
}
static void validateAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("유효하지 않은 나이: " + age);
}
System.out.println("유효한 나이: " + age);
}
public static void main(String[] args) {
// Checked: 반드시 try-catch 또는 throws로 처리
try {
String content = readFile("nonexistent.txt");
} catch (FileNotFoundException e) {
System.out.println("Checked 예외: " + e.getMessage());
}
// Unchecked: 처리하지 않아도 컴파일 에러 없음 (권장은 함)
try {
int result = divide(10, 0);
} catch (IllegalArgumentException e) {
System.out.println("Unchecked 예외: " + e.getMessage());
}
validateAge(25);
// validateAge(-5); // IllegalArgumentException 발생
}
}
사용자 정의 예외
도메인에 특화된 예외를 만들어 의미 있는 에러 처리를 합니다.
// 사용자 정의 Checked 예외
class InsufficientBalanceException extends Exception {
private final long currentBalance;
private final long requestedAmount;
InsufficientBalanceException(long currentBalance, long requestedAmount) {
super(String.format("잔액 부족: 현재 %,d원, 요청 %,d원",
currentBalance, requestedAmount));
this.currentBalance = currentBalance;
this.requestedAmount = requestedAmount;
}
public long getShortfall() {
return requestedAmount - currentBalance;
}
}
// 사용자 정의 Unchecked 예외
class InvalidAccountException extends RuntimeException {
InvalidAccountException(String accountNumber) {
super("유효하지 않은 계좌번호: " + accountNumber);
}
}
class BankService {
private long balance;
BankService(long initialBalance) {
this.balance = initialBalance;
}
void withdraw(long amount) throws InsufficientBalanceException {
if (amount <= 0) {
throw new IllegalArgumentException("출금액은 양수여야 합니다.");
}
if (amount > balance) {
throw new InsufficientBalanceException(balance, amount);
}
balance -= amount;
System.out.println(String.format("%,d원 출금 완료. 잔액: %,d원", amount, balance));
}
long getBalance() {
return balance;
}
}
public class CustomExceptionExample {
public static void main(String[] args) {
BankService bank = new BankService(100000);
try {
bank.withdraw(50000); // 성공
bank.withdraw(80000); // 잔액 부족
} catch (InsufficientBalanceException e) {
System.out.println(e.getMessage());
System.out.println("부족한 금액: " + String.format("%,d원", e.getShortfall()));
}
try {
bank.withdraw(-1000); // 잘못된 금액
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
}
오늘의 연습문제
-
입력 검증기: 사용자 이름, 이메일, 비밀번호를 검증하는
Validator클래스를 만드세요. 각 검증 실패 시ValidationException(사용자 정의)을 던지고, 여러 검증 에러를 모아서 한 번에 보고하세요. -
파일 처리기: try-with-resources를 사용하여 리소스(커스텀
Connection클래스)를 열고, 작업 중 예외가 발생해도 리소스가 안전하게 닫히는 것을 확인하는 프로그램을 작성하세요. -
예외 체이닝:
DataAccessException이SQLException을 감싸고(wrapping),ServiceException이DataAccessException을 감싸는 3단계 예외 체이닝을 구현하세요.getCause()로 원인 예외를 추적하세요.