프로그램을 만들다 보면 파일이 없거나, 숫자를 0으로 나누거나, 없는 인덱스에 접근하는 상황이 반드시 생긴다. 이런 상황을 그냥 무시하면 프로그램이 죽어버리는데, 어떻게 대응해야 할까?

예외 처리는 프로그램 실행 중 발생할 수 있는 문제에 적절히 대응하는 메커니즘 이다. 단순히 "안 죽게 만드는 기술"이 아니라, 문제가 생겼을 때 어디서 어떻게 처리할지를 설계하는 것 에 가깝다.

Error vs Exception

Java에서 "에러"와 "예외"는 명확하게 구분된다. 둘 다 Throwable을 상속받지만 성격이 다르다.

PLAINTEXT
              Throwable
              /       \
          Error      Exception
            |          /        \
  OutOfMemoryError  IOException  RuntimeException
                                   |
                              NullPointerException
  • Error: 시스템 레벨의 심각한 문제. OutOfMemoryError, StackOverflowError 같은 것들이다. 코드로 처리할 수 없다.
  • Exception: 프로그램 로직에서 발생할 수 있는 문제. 코드로 처리할 수 있다.

try-catch-finally

예외가 발생할 수 있는 코드를 try에 넣고, 발생했을 때의 처리를 catch에 작성한다.

JAVA
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("0으로 나눌 수 없습니다: " + e.getMessage());
}
System.out.println("프로그램이 계속 실행됩니다");

try-catch가 없었다면 프로그램이 그대로 종료됐을 것이다. 예외를 잡았기 때문에 이후 코드가 정상적으로 실행된다.

finally는 예외 발생 여부와 상관없이 ** 항상 실행 **된다. 주로 리소스 정리에 쓰인다. try에서 return을 해도 finally는 실행된다.

catch 순서가 중요하다

여러 catch를 쓸 때는 ** 구체적인 예외를 먼저, 포괄적인 예외를 나중에** 써야 한다. Exception을 맨 위에 쓰면 아래 catch가 절대 실행되지 않아 컴파일 에러가 난다.

JAVA
try { ... }
catch (FileNotFoundException e) { ... }  // 구체적
catch (IOException e) { ... }            // 포괄적
catch (Exception e) { ... }              // 최후의 안전망

같은 방식으로 처리할 예외가 여러 개라면 Java 7+에서 |로 묶을 수 있다.

JAVA
catch (NullPointerException | IllegalArgumentException e) {
    System.out.println("잘못된 입력: " + e.getMessage());
}

Checked vs Unchecked — 가장 중요한 구분

Checked 예외

Exception을 상속받되 RuntimeException은 상속받지 ** 않는** 예외다. 컴파일러가 try-catch 또는 throws 선언을 ** 강제 **한다. 파일, 네트워크, DB처럼 ** 외부 환경 **과 소통할 때 주로 발생한다.

JAVA
// try-catch 없이 쓰면 컴파일 에러
FileReader reader = new FileReader("없는파일.txt");

Unchecked 예외

RuntimeException을 상속받는 예외다. 컴파일러가 처리를 강제하지 않는다. null 참조, 잘못된 인덱스처럼 ** 코드의 실수 **로 발생한다.

구분CheckedUnchecked
상속Exception (RuntimeException 제외)RuntimeException
컴파일러 검사O — 반드시 처리해야 함X — 처리 안 해도 컴파일 됨
발생 원인외부 환경 (파일, 네트워크, DB)프로그래밍 실수 (null, 잘못된 인덱스)
대표 예외IOException, SQLExceptionNullPointerException, IllegalArgumentException

Checked는 "외부 세계와 소통할 때 생기는 예외", Unchecked는 "내 코드의 실수로 생기는 예외"로 기억하면 구분하기 쉽다.

throws와 throw

throws — 예외를 호출자에게 떠넘기기

예외를 직접 처리하지 않고 ** 호출한 쪽에 처리 책임을 넘긴다 **.

JAVA
static String readFile(String path) throws IOException {
    BufferedReader reader = new BufferedReader(new FileReader(path));
    return reader.readLine();
}

throws를 계속 떠넘기면 결국 main까지 올라가고, main에서도 떠넘기면 JVM이 프로그램을 종료시킨다. 적절한 계층에서 잡아서 처리하는 게 좋다.

throw — 예외를 직접 발생시키기

개발자가 의도적으로 예외를 던진다.

JAVA
static void setAge(int age) {
    if (age < 0) {
        throw new IllegalArgumentException("나이는 음수일 수 없습니다: " + age);
    }
}
구분throwthrows
위치메서드 ** 안**메서드 ** 선언부**
역할예외를 ** 발생 **시킨다예외를 ** 떠넘긴다 **고 선언

try-with-resources — 자동 리소스 관리

파일이나 DB 연결은 사용 후 반드시 닫아야 한다. 기존에는 finally에서 직접 닫았는데, finally 안에 또 try-catch가 들어가는 불편함이 있었다.

Java 7의 try-with-resources는 AutoCloseable을 구현한 객체를 ** 자동으로 닫아준다 **.

JAVA
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    System.out.println(reader.readLine());
} catch (IOException e) {
    System.out.println("에러: " + e.getMessage());
}
// reader.close() 호출 불필요 — 자동으로 닫힘

여러 리소스를 세미콜론으로 구분해서 선언할 수 있으며, 닫히는 순서는 ** 선언의 역순 **이다. Java의 I/O 관련 클래스들(InputStream, Reader, Connection 등)은 대부분 AutoCloseable을 이미 구현하고 있다.

커스텀 예외

기본 제공 예외만으로 부족할 때 직접 만들 수 있다. RuntimeException을 상속하면 Unchecked, Exception을 상속하면 Checked 예외가 된다.

JAVA
public class InsufficientBalanceException extends RuntimeException {
    private final int currentBalance;
    private final int withdrawAmount;

    public InsufficientBalanceException(int current, int withdraw) {
        super("잔액 부족: 현재 " + current + "원, 출금 요청 " + withdraw + "원");
        this.currentBalance = current;
        this.withdrawAmount = withdraw;
    }
}

커스텀 예외를 만들 때는 이름을 ~Exception으로 끝내고, 의미 있는 메시지를 super()로 전달하며, 필요하면 추가 정보를 필드로 담는다. 원인 예외가 있으면 Throwable cause를 받는 생성자도 만들어두자.

주의할 점

빈 catch 블록은 최악의 패턴이다

JAVA
// 절대 이러지 말 것
try { ... }
catch (Exception e) { }

예외를 잡고 아무것도 안 하면 문제가 숨겨진다. 나중에 디버깅할 때 어디서 문제가 생겼는지 전혀 알 수 없게 된다. 최소한 로그라도 남기고, 필요하면 다시 던져야 한다.

예외를 흐름 제어에 쓰지 말자

JAVA
// 나쁜 예 — 예외를 if문처럼 사용
try {
    int value = Integer.parseInt(input);
} catch (NumberFormatException e) {
    value = 0;
}

예외 처리는 스택 트레이스 생성 등 비용이 크다. 정상적인 흐름 제어에 예외를 쓰면 성능도 떨어지고 코드 의도도 불명확해진다. 먼저 검증하고 변환하는 방식이 낫다.

예외 변환 시 원인을 보존하자

JAVA
try {
    readFromDatabase();
} catch (SQLException e) {
    // 원인 예외(e)를 함께 전달
    throw new ServiceException("사용자 조회 실패", e);
}

원인 예외를 넘기지 않으면 스택 트레이스에서 근본 원인을 추적할 수 없다. 예외를 감쌀 때는 반드시 Throwable cause를 함께 전달하자.

finally에서 return은 쓰지 말자

finally에서 return을 쓰면 try의 반환값을 ** 덮어쓴다 **. 의도치 않은 버그를 만들기 딱 좋으므로, finally에서는 리소스 정리만 하자.

정리

개념핵심
Error vs ExceptionError는 시스템 문제(처리 불가). Exception은 프로그램 문제(처리 가능)
try-catch-finally예외를 잡고 처리. finally는 항상 실행
Checked 예외컴파일러가 처리 강제. 외부 환경 관련 (IOException 등)
Unchecked 예외처리 강제 안 함. 프로그래밍 실수 (NullPointerException 등)
throws / throwthrows는 떠넘기기 선언. throw는 직접 발생
try-with-resourcesAutoCloseable 객체를 자동으로 닫아줌 (Java 7+)
커스텀 예외RuntimeException(Unchecked) 또는 Exception(Checked) 상속
빈 catch 블록문제를 숨기는 최악의 패턴. 로그라도 남길 것
댓글 로딩 중...