← Java 강의 목록으로
📂
예외 · 입출력
예외 · 입출력 · 선수: 이전 단원

15. 예외처리

프로그램이 실행 중 만나는 비정상 상황(파일 없음·잘못된 입력·네트워크 장애 등) 을 안전하게 다루는 메커니즘이 **예외(exception) 처리** 입니다. Java 는 try/catch/finally 와 checked/unchecked 라는 두 종류의 예외 체계를 제공합니다.

Java예외I/O예외처리
소요 시간
약 1~1.5시간
난이도
📊 중급
선수 조건
🎯 이전 단원 또는 동등 지식
결과물
프로그램이 실행 중 만나는 비정상 상황(파일 없음·잘못된 입력·네트워크 장애 등) 을 안전하게 다루는 메커니즘이 **예외(exception) 처리** 입니다. Java 는 try/catch/finally 와 checked/unchecked 라는 두 종류의 예외 체계를 제공합니다.

이 강의에서 배우는 것

  • 1`try` / `catch` / `finally` 의 흐름을 안다
  • 2checked 예외와 unchecked(RuntimeException) 의 차이를 안다
  • 3`throws` 로 예외를 위임할 수 있다
  • 4사용자 정의 예외 클래스를 만든다
  • 5multi-catch 와 예외 체이닝(`cause`) 을 본다

소개

프로그램이 실행 중 만나는 비정상 상황(파일 없음·잘못된 입력·네트워크 장애 등) 을 안전하게 다루는 메커니즘이 **예외(exception) 처리** 입니다. Java 는 try/catch/finally 와 checked/unchecked 라는 두 종류의 예외 체계를 제공합니다.

핵심 개념

1) try / catch / finally

java
try {
    int n = Integer.parseInt("abc");
} catch (NumberFormatException e) {
    System.out.println("숫자 아님: " + e.getMessage());
} finally {
    System.out.println("항상 실행");
}

`finally` 는 예외 여부와 상관없이 **무조건** 실행됩니다 (자원 정리에 사용).

2) Checked vs Unchecked

종류예시의무 처리
Checked (`Exception` 자손)`IOException`, `SQLException`반드시 `try/catch` 또는 `throws` 선언
Unchecked (`RuntimeException` 자손)`NullPointerException`, `IllegalArgumentException`강제 처리 없음
Error`OutOfMemoryError`처리 시도 X (시스템 오류)

3) `throws`

java
static void readFile(String path) throws IOException {
    Files.readString(Path.of(path));
}

호출자에게 "이 예외를 처리하라" 고 떠넘기는 키워드입니다.

4) 사용자 정의 예외

java
class InvalidAgeException extends RuntimeException {
    public InvalidAgeException(String msg) { super(msg); }
}

도메인 의미가 분명한 예외 클래스를 만들면 호출자가 처리 흐름을 명확히 짤 수 있습니다.

5) Multi-catch

java
try {
    ...
} catch (IOException | NumberFormatException e) {
    System.out.println("실패: " + e.getMessage());
}

JDK 7+ 부터 한 번의 catch 절에 여러 예외 타입을 허용합니다.

핵심 예제

예제 1 — `TryCatch.java` : 기본 흐름

java
public class TryCatch {
    public static void main(String[] args) {
        try {
            int n = Integer.parseInt("abc");
            System.out.println(n);
        } catch (NumberFormatException e) {
            System.out.println("숫자 아님: " + e.getMessage());
        } finally {
            System.out.println("종료 정리");
        }
        System.out.println("프로그램 계속");
    }
}

**실행 결과**

text
숫자 아님: For input string: "abc"
종료 정리
프로그램 계속

**메모:** catch 가 예외를 잡으면 프로그램은 죽지 않고 그 다음 줄을 계속 실행합니다.

예제 2 — `MultiCatch.java` : 여러 예외 동시 처리

java
public class MultiCatch {
    public static void main(String[] args) {
        Object[] inputs = { "10", "abc", null };
        for (Object o : inputs) {
            try {
                String s = (String) o;
                int n = Integer.parseInt(s.trim());
                System.out.println("성공: " + n);
            } catch (NullPointerException | NumberFormatException e) {
                System.out.println("실패: " + e.getClass().getSimpleName());
            }
        }
    }
}

**실행 결과**

text
성공: 10
실패: NumberFormatException
실패: NullPointerException

**메모:** 두 예외가 의미적으로 같은 처리라면 한 줄로 묶을 수 있습니다.

예제 3 — `ThrowsAndCustom.java` : `throws` + 사용자 예외

java
public class ThrowsAndCustom {
    static class InvalidAgeException extends RuntimeException {
        public InvalidAgeException(String msg) { super(msg); }
    }

    static void register(String name, int age) {
        if (age < 0) {
            throw new InvalidAgeException("나이는 0 이상: " + age);
        }
        System.out.println("등록: " + name + " (" + age + ")");
    }

    public static void main(String[] args) {
        try {
            register("지수", 21);
            register("미상", -1);
        } catch (InvalidAgeException e) {
            System.out.println("거부: " + e.getMessage());
        }
    }
}

**실행 결과**

text
등록: 지수 (21)
거부: 나이는 0 이상: -1

**메모:** unchecked 예외(`RuntimeException` 상속) 는 `throws` 선언이 필요 없습니다.

예제 4 — `ExceptionChaining.java` : 예외 원인 보존

java
public class ExceptionChaining {
    static void load() {
        try {
            Integer.parseInt("nope");
        } catch (NumberFormatException low) {
            throw new IllegalStateException("설정 로드 실패", low);
        }
    }

    public static void main(String[] args) {
        try {
            load();
        } catch (IllegalStateException e) {
            System.out.println("바깥: " + e.getMessage());
            System.out.println("원인: " + e.getCause());
        }
    }
}

**실행 결과**

text
바깥: 설정 로드 실패
원인: java.lang.NumberFormatException: For input string: "nope"

**메모:** 낮은 레벨의 예외를 상위 의미로 **감싸면서도 원인을 보존** 하는 패턴은 디버깅에 매우 유용합니다.

전체 예제 코드 (src/)

src/ExceptionChaining.java

java
public class ExceptionChaining {
    static void load() {
        try {
            Integer.parseInt("nope");
        } catch (NumberFormatException low) {
            throw new IllegalStateException("설정 로드 실패", low);
        }
    }

    public static void main(String[] args) {
        try {
            load();
        } catch (IllegalStateException e) {
            System.out.println("바깥: " + e.getMessage());
            System.out.println("원인: " + e.getCause());
        }
    }
}

src/MultiCatch.java

java
public class MultiCatch {
    public static void main(String[] args) {
        Object[] inputs = { "10", "abc", null };
        for (Object o : inputs) {
            try {
                String s = (String) o;
                int n = Integer.parseInt(s.trim());
                System.out.println("성공: " + n);
            } catch (NullPointerException | NumberFormatException e) {
                System.out.println("실패: " + e.getClass().getSimpleName());
            }
        }
    }
}

src/ThrowsAndCustom.java

java
public class ThrowsAndCustom {
    static class InvalidAgeException extends RuntimeException {
        public InvalidAgeException(String msg) { super(msg); }
    }

    static void register(String name, int age) {
        if (age < 0) {
            throw new InvalidAgeException("나이는 0 이상: " + age);
        }
        System.out.println("등록: " + name + " (" + age + ")");
    }

    public static void main(String[] args) {
        try {
            register("지수", 21);
            register("미상", -1);
        } catch (InvalidAgeException e) {
            System.out.println("거부: " + e.getMessage());
        }
    }
}

src/TryCatch.java

java
public class TryCatch {
    public static void main(String[] args) {
        try {
            int n = Integer.parseInt("abc");
            System.out.println(n);
        } catch (NumberFormatException e) {
            System.out.println("숫자 아님: " + e.getMessage());
        } finally {
            System.out.println("종료 정리");
        }
        System.out.println("프로그램 계속");
    }
}

자주 하는 실수

  1. `catch (Exception e)` 로 모든 예외 통째로 잡고 무시 (silent failure)
  2. 빈 catch 블록 → 문제 묻어 버림
  3. `finally` 에서 `return` 사용해 본문 return 을 덮어씀
  4. checked 예외를 throws 도 try/catch 도 없이 무시 시도 (컴파일 에러)
  5. 도메인 의미 없는 `RuntimeException` 남발 (사용자 정의 예외가 더 명확)

정리

  • 예외는 비정상 흐름을 표현하는 **객체**
  • checked 는 반드시 처리, unchecked 는 선택적 처리
  • 사용자 정의 예외로 도메인 의미를 분명히
  • 원인을 보존하면 추적이 쉬워짐

과제

# 과제 - 15. 예외처리

## 문제 1 — 안전한 나눗셈

  • 파일명: `Homework01.java`
  • 핵심 개념: `try`/`catch`, `ArithmeticException`

요구사항

  • `safeDiv(int a, int b)` 메서드: 0 으로 나누면 `0` 반환, 그 외엔 결과 반환.
  • `(10,2)`, `(10,0)`, `(7,3)` 호출 결과 출력.

예상 출력

text
10 / 2 = 5
10 / 0 = 0
7 / 3 = 2

## 문제 2 — 사용자 정의 예외

  • 파일명: `Homework02.java`
  • 핵심 개념: 커스텀 예외 + `throw`

요구사항

  • `class NotEnoughBalanceException extends RuntimeException` 정의.
  • `withdraw(balance, amount)` : amount > balance 면 위 예외, 아니면 차감 후 잔액 반환.
  • (1000, 300), (1000, 2000) 시나리오 처리.

예상 출력

text
잔액: 700
실패: 잔액 부족 1000 < 2000

## 정답 확인 직접 풀어 본 후 [`answer/`](./answer/) 폴더의 정답과 비교해 보세요.

정답 코드 (homework/answer/)

answer/Homework01.java

java
/** 안전한 정수 나눗셈. */
public class Homework01 {
    public static void main(String[] args) {
        int[][] cases = { {10, 2}, {10, 0}, {7, 3} };
        for (int[] c : cases) {
            System.out.println(c[0] + " / " + c[1] + " = " + safeDiv(c[0], c[1]));
        }
    }

    static int safeDiv(int a, int b) {
        try {
            return a / b;
        } catch (ArithmeticException e) {
            return 0;
        }
    }
}

answer/Homework02.java

java
/** 사용자 정의 예외: 잔액 부족. */
public class Homework02 {
    static class NotEnoughBalanceException extends RuntimeException {
        public NotEnoughBalanceException(String msg) { super(msg); }
    }

    static long withdraw(long balance, long amount) {
        if (amount > balance) {
            throw new NotEnoughBalanceException("잔액 부족 " + balance + " < " + amount);
        }
        return balance - amount;
    }

    public static void main(String[] args) {
        try {
            long b1 = withdraw(1000, 300);
            System.out.println("잔액: " + b1);
            long b2 = withdraw(1000, 2000);
            System.out.println("잔액: " + b2);
        } catch (NotEnoughBalanceException e) {
            System.out.println("실패: " + e.getMessage());
        }
    }
}

직접 해 보기

bash
cd 04_예외_입출력/15_예외처리/src
javac ThrowsAndCustom.java
java ThrowsAndCustom

다음 단원

[16_파일_IO](../16_파일_IO/) — `Files`, `Path`, try-with-resources 를 배웁니다.

예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗