Java 26일 코스 - Day 14: 예외 처리

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());
        }
    }
}

오늘의 연습문제

  1. 입력 검증기: 사용자 이름, 이메일, 비밀번호를 검증하는 Validator 클래스를 만드세요. 각 검증 실패 시 ValidationException(사용자 정의)을 던지고, 여러 검증 에러를 모아서 한 번에 보고하세요.

  2. 파일 처리기: try-with-resources를 사용하여 리소스(커스텀 Connection 클래스)를 열고, 작업 중 예외가 발생해도 리소스가 안전하게 닫히는 것을 확인하는 프로그램을 작성하세요.

  3. 예외 체이닝: DataAccessExceptionSQLException을 감싸고(wrapping), ServiceExceptionDataAccessException을 감싸는 3단계 예외 체이닝을 구현하세요. getCause()로 원인 예외를 추적하세요.

이 글이 도움이 되었나요?