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를 계산할 때:

  1. 먼저 두 정수를 계산 → 결과는 정수 2
  2. 이 정수 값을 result 변수에 할당 → 2.0

resultfloat인지 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 메서드에서 예외 처리를 호출자에게 위임

주의할 점

  1. switch문에서 break 잊지 말기 (또는 Java 14+ switch expression 사용)
  2. 정수 나눗셈 결과는 정수 - 실수 결과가 필요하면 캐스팅 필요
  3. try-with-resources로 리소스 누수 방지 - 파일, DB 연결 등에 활용