예외란 try-catch finally 예외 계층 throws · throw 다중 catch 📝 문제풀이
◀ 이전 📋 목차 CH 15 ▶
🛡️ CHAPTER 14 · 고급

프로그램이 죽지 않게 지키는 법

아무리 잘 짠 프로그램도 실행 중에 예상 못 한 일이 터져요. 파일이 없거나, 0으로 나누거나, 없는 값을 건드리거나. 이번 장에서는 그런 상황에서 프로그램이 뻗지 않고 우아하게 대응하는 예외 처리(Exception Handling)를 단계별로 배웁니다.

🎯 이 장을 끝내면
💥
개요
예외(Exception)가 뭔가요?
"실행 중에 터지는 오류"를 먼저 감 잡아봐요.

예외(Exception)는 프로그램이 실행되는 도중(런타임, runtime)에 발생하는 오류예요. 문법은 멀쩡해서 컴파일도 잘 됐고 실행도 시작됐는데, 돌아가는 중에 예상치 못한 일이 생겨 정상 흐름이 막히는 상황이죠. 예를 들어 "있지도 않은 파일을 열려고 하거나", "숫자를 0으로 나누거나", "비어 있는(null) 값을 건드리는" 경우예요.

🔤
런타임(runtime) — 프로그램이 실제로 실행되고 있는 동안을 뜻해요. 반대로 코드를 번역하는 때는 "컴파일 타임"이라고 해요. 예외는 컴파일 타임이 아니라 런타임에 터지는 오류라, 실행해 보기 전엔 드러나지 않는 경우가 많아요.
🧩 컴파일 에러 vs 예외(런타임 오류) — 언제 터지나? ① 컴파일 에러(Compile Error)코드를 번역(컴파일)하는 단계에서 걸려요. 세미콜론을 빼먹거나, 없는 변수를 쓰거나, 타입이 안 맞는 것들. 프로그램이 아예 실행되지도 못해요.
② 예외(Exception, 런타임 오류)컴파일은 통과했고 프로그램이 실행 중인데, 그때 가서 문제가 터져요. 컴파일러는 몰랐던 일이죠. 한 줄 정리: 컴파일 에러는 "실행 전에", 예외는 "실행 중에" 발생해요. 이번 장의 주인공은 실행 중에 터지는 예외입니다.

그럼 왜 예외를 "처리"해야 할까요? 예외가 발생했는데 아무 대응도 없으면, 프로그램은 그 자리에서 비정상 종료(뻗어요)돼요. 은행 앱이 잔액 조회 한 번 실패했다고 통째로 꺼지면 곤란하겠죠. 예외 처리는 "문제가 생겨도 프로그램이 죽지 않고, 사용자에게 우아하게 안내하거나 대체 동작을 하도록" 만드는 안전장치예요.

🥅
안전망
곡예사가 떨어져도(예외 발생) 바닥에 부딪히지 않게 받쳐줘요.
🚪
비상구
문제가 터지면 정상 흐름 대신 정해진 대피 경로(catch)로 빠져요.
🛟
프로그램은 생존
한 번 실패해도 앱 전체가 꺼지지 않고 계속 돌아가요.
⚖️ 에러(Error) vs 예외(Exception) — 둘 다 문제지만 성격이 달라요 에러(Error) — 개발자가 손쓰기 어려운 심각한 문제. 메모리 부족(OutOfMemoryError)처럼 시스템 수준의 고장이에요. 보통 잡아서 처리하지 않아요(잡아도 딱히 할 수 있는 게 없음).
예외(Exception) — 개발자가 충분히 대응할 수 있는 문제. 파일이 없거나, 잘못된 입력이 들어오거나 하는 경우. 이번 장에서 다루는 게 바로 이 예외예요. 💡 기억할 것: 우리가 try-catch로 잡아서 처리하는 대상은 주로 예외(Exception)예요. 에러(Error)는 잡는 대상이 아니라고 생각하면 편해요.
Exception 런타임 오류 try-catch finally Throwable checked unchecked throw throws
🧭
이번 장의 목표. "오류가 나면 무조건 나쁜 것"이 아니라, "오류가 날 수 있음을 예상하고 대비하는 것"이 예외 처리예요. 위험한 코드를 안전하게 감싸는 습관을 몸에 익혀봅시다.
🖼️ 그림으로 보기 — 예외 계층 구조
Throwable 모든 오류의 최상위 Error 심각한 시스템 오류 · 안 잡음 Exception 개발자가 대응 가능 (try-catch 대상) RuntimeException unchecked NPE · Arithmetic · IndexOutOfBounds 그 외 Exception checked · 처리 강제 IOException · SQLException
최상위는 Throwable, 그 아래 Error(시스템 고장, 보통 안 잡음)와 Exception이 있어요. Exception은 다시 RuntimeException(unchecked)그 외(checked)로 나뉘죠. checked는 컴파일러가 "처리하라"고 강제하고, unchecked는 강제하지 않아요.
🎣
기본
try-catch로 예외를 잡아요
위험한 코드는 try에, 대응 코드는 catch에.

예외 처리의 기본은 try-catch예요. 구조는 아주 단순합니다. 위험할 수 있는 코드를 try 블록 안에 넣고, 거기서 예외가 터지면 catch 블록으로 흐름이 넘어가 대응 코드를 실행해요.

try {
    // ⚠️ 예외가 발생할 수 있는 위험한 코드
    int result = 10 / 0;      // 여기서 ArithmeticException 발생!
    System.out.println(result); // ← 이 줄은 실행되지 않아요
} catch (ArithmeticException e) {
    // 🎣 위에서 예외가 터지면 여기로 점프
    System.out.println("0으로 나눌 수 없어요!");
}
System.out.println("프로그램은 계속 진행됩니다.");

위 코드에서 10 / 00으로 나누는 잘못된 연산이라 ArithmeticException이 발생해요. 그 순간 try 안의 나머지 줄은 건너뛰고 곧바로 catch로 점프합니다. 중요한 건, 예외를 잡았기 때문에 프로그램이 죽지 않고 마지막 줄까지 계속 실행된다는 거예요.

🔎 위 코드의 실행 순서 1. try 진입 → 10 / 0 실행 시도
2. 💥 ArithmeticException 발생 → println(result)건너뜀
3. catch로 점프 → "0으로 나눌 수 없어요!" 출력
4. try-catch를 빠져나와 → "프로그램은 계속 진행됩니다." 출력 즉, 예외가 나면 try의 나머지는 실행되지 않고 catch로 넘어가요. 이게 try-catch의 핵심 흐름이에요.

또 다른 흔한 예로 배열의 범위를 벗어난 접근이 있어요. 길이가 3인 배열에서 4번째 칸을 읽으려 하면 ArrayIndexOutOfBoundsException이 발생해요.

int[] arr = {10, 20, 30};   // 인덱스는 0, 1, 2 뿐
try {
    System.out.println(arr[5]);  // 💥 없는 5번 칸 → ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("배열 범위를 벗어났어요!");
    System.out.println("메시지: " + e.getMessage()); // 예외가 담고 있는 정보
}
▶️
try 진입
위험한 코드 실행 시작
💥
예외 발생
try의 나머지는 건너뜀
🎣
catch 실행
예외를 잡아 대응
계속 진행
프로그램 죽지 않음
💡
catch의 e는 예외 객체예요. 예외가 발생하면 자바가 그 정보를 담은 객체를 만들어 catch (예외타입 e)e로 넘겨줘요. e.getMessage()로 메시지를, e.printStackTrace()로 어디서 터졌는지 자세히 볼 수 있어요.
🧹
마무리 블록
finally — 무슨 일이 있어도 실행돼요
예외가 나든 안 나든, 마지막에 꼭 한 번.

finally 블록은 예외 발생 여부와 상관없이 항상 실행되는 마무리 코드예요. try가 무사히 끝났든, 중간에 예외가 터져 catch로 넘어갔든, 마지막에는 반드시 finally를 거쳐요. 그래서 자원 정리(파일 닫기, 연결 끊기 등)처럼 "어떤 경우든 꼭 해야 하는 일"에 씁니다.

🔤
용어 — 자원(resource)은 파일, 네트워크 연결, 데이터베이스 커넥션처럼 다 쓰고 나면 반드시 닫아서(close) 반납해야 하는 것들을 말해요. 안 닫으면 조금씩 새어(누수) 나중에 프로그램이 느려지거나 뻗을 수 있어요.
try {
    System.out.println("1. try 시작");
    int x = 10 / 0;                 // 💥 예외 발생
    System.out.println("2. 이 줄은 실행 안 됨");
} catch (ArithmeticException e) {
    System.out.println("3. catch: 예외 처리");
} finally {
    System.out.println("4. finally: 항상 실행!");
}
System.out.println("5. try-catch 종료 후");
🖨️ 위 코드의 실제 출력 1. try 시작
3. catch: 예외 처리
4. finally: 항상 실행!
5. try-catch 종료 후 2번은 예외 때문에 건너뛰었지만, 4번 finally는 예외가 났어도 실행됐어요. 만약 예외가 안 났다면 순서는 1 → 2 → 4 → 5가 됩니다(catch는 건너뜀). 어느 경우든 finally는 항상 실행돼요.
상황trycatchfinally
예외 ✅ 전부 실행⛔ 건너뜀✅ 실행
예외 발생⚠️ 예외 지점까지만✅ 실행✅ 실행
🧹
finally의 존재 이유. 파일이나 네트워크 연결을 열었다면, 성공하든 실패하든 반드시 닫아야 자원이 새지 않아요. 그래서 "닫기" 코드를 finally에 두면, 예외가 나도 확실히 정리됩니다. (뒤에서 배울 try-with-resources가 이걸 더 편하게 해줘요.)
🧬
계층 구조
예외에도 족보가 있어요
최상위는 Throwable. checked와 unchecked를 구분해봐요.

자바의 모든 예외와 에러는 Throwable이라는 최상위 클래스에서 갈라져 나와요. 이 Throwable 아래로 크게 두 갈래, ErrorException이 있어요.

Throwable ← 모든 예외·에러의 최상위 조상
 ├─ Error ← 심각한 시스템 문제 (예: OutOfMemoryError) — 보통 안 잡음
 └─ Exception ← 우리가 다루는 예외
    ├─ IOException ← checked (컴파일러가 처리 강제)
    └─ RuntimeException ← unchecked (처리 강제 안 함)
        ├─ NullPointerException
        ├─ ArithmeticException
        └─ ArrayIndexOutOfBoundsException

여기서 중요한 건 Exception 아래를 다시 두 종류로 나눈다는 점이에요. RuntimeException 계열이냐 아니냐로 갈립니다.

📌 checked 예외 vs unchecked 예외 checked 예외RuntimeException아닌 Exception들. 컴파일러가 "이거 처리 안 하면 컴파일 안 해줄 거야"라고 강제해요. 그래서 반드시 try-catch로 잡거나 throws로 떠넘겨야 컴파일이 돼요. 대표 예: 파일 입출력의 IOException.
unchecked 예외RuntimeException과 그 자식들. 컴파일러가 처리를 강제하지 않아요. 주로 프로그래머의 실수로 생기는 것들이라, 안 잡아도 컴파일은 됩니다(대신 실행 중에 터짐). 대표 예: NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException. 💡 외우는 요령: "Runtime이면 unchecked". RuntimeException 밑에 있으면 unchecked, 아니면 checked예요.
구분checked 예외unchecked 예외
뿌리Exception (RuntimeException 제외)RuntimeException 계열
컴파일러 처리 강제✅ 반드시 처리해야 컴파일됨⛔ 강제하지 않음
주로 생기는 이유외부 요인(파일 없음, 네트워크 등)프로그래머의 실수(로직 오류)
대표 예IOExceptionNullPointerException
ArithmeticException
ArrayIndexOutOfBoundsException
🧭
왜 이 구분이 중요할까요? checked 예외는 코드에서 반드시 처리를 명시해야 컴파일이 되기 때문에, "이 메서드는 실패할 수 있음"을 코드가 스스로 알려줘요. unchecked는 강제가 없는 대신, 우리가 스스로 조심(null 검사, 범위 검사)해야 하죠.
🧪 번외 — 자주 만나는 unchecked 예외 3형제
1. NullPointerException (NPE) null인 변수를 마치 객체인 것처럼 사용(메서드 호출·필드 접근)하려 할 때 발생해요. 예: String s = null; s.length(); → 없는 걸 건드려서 💥. 자바에서 가장 흔한 예외로 꼽혀요. 2. ArithmeticException 정수를 0으로 나눌 때 발생해요. 예: 10 / 0. (참고로 실수 10.0 / 0은 예외 대신 Infinity가 나와요.) 3. ArrayIndexOutOfBoundsException 배열에서 존재하지 않는 인덱스에 접근할 때 발생해요. 길이 3인 배열의 arr[5] 같은 경우죠. 셋 다 RuntimeException(unchecked)이라는 공통점이 있어요.
📤
직접 발생 · 떠넘기기
throw와 throws — 한 글자 차이, 완전 다른 일
throw는 "던지기", throws는 "넘긴다고 선언".

throw(s 없음)예외를 내가 직접 발생시키는 명령이에요. "이건 잘못된 상황이야!" 하고 예외를 던지는 거죠. 주로 잘못된 입력값을 걸러낼 때 씁니다.

void setAge(int age) {
    if (age < 0) {
        // 🚨 나이가 음수면 내가 직접 예외를 "던진다"
        throw new IllegalArgumentException("나이는 음수일 수 없어요: " + age);
    }
    System.out.println("나이 설정: " + age);
}

위에서 throw new IllegalArgumentException(...)예외 객체를 새로 만들어(new) 던지는 코드예요. 이렇게 하면 잘못된 값이 들어왔을 때 그 자리에서 실행이 멈추고, 호출한 쪽이 이 예외를 받아 처리하게 됩니다.

한편 throws(s 있음)메서드 선언부에 붙여서 "이 메서드는 이런 예외를 낼 수 있으니, 처리는 나를 부른 쪽이 해줘"라고 떠넘기는(위임하는) 선언이에요. 특히 checked 예외를 직접 잡지 않고 위로 넘길 때 필수예요.

// 📤 "나는 IOException을 낼 수 있어. 처리는 호출자가 해."
void readFile(String path) throws IOException {
    // 파일을 여는 코드 (IOException은 checked 예외)
    // 여기서 try-catch로 잡지 않고, throws로 호출자에게 넘김
}

// 호출하는 쪽이 처리 책임을 짐
void run() {
    try {
        readFile("data.txt");
    } catch (IOException e) {
        System.out.println("파일을 읽지 못했어요.");
    }
}
⚔️ throw vs throws 한눈에 throw메서드 안에서 사용. 예외를 실제로 발생(던지기)시켜요. 뒤에 예외 객체가 와요. 예: throw new IllegalArgumentException(...);
throws메서드 선언부(괄호 뒤)에 사용. "이 메서드는 이런 예외를 낼 수 있다"고 선언·위임해요. 뒤에 예외 클래스 이름이 와요. 예: void m() throws IOException 💡 헷갈리면: throw = 던진다(행동), throws = 던질 수 있다고 알린다(선언). s 하나 차이지만 하는 일이 완전히 달라요.
💡
checked 예외와 throws. IOException 같은 checked 예외는 반드시 try-catch로 잡거나 throws로 넘겨야 컴파일돼요. "여기서 처리하기 애매하다" 싶으면 throws로 넘기고, 최종적으로 책임질 수 있는 곳에서 try-catch로 잡는 게 흔한 패턴이에요.
🧰
한 걸음 더
다중 catch · try-with-resources
여러 예외를 나눠 잡고, 자원을 자동으로 닫아요.

try에서 서로 다른 예외가 여러 개 발생할 수 있다면, catch를 여러 개 이어 붙여 예외마다 다르게 대응할 수 있어요. 발생한 예외와 타입이 맞는 첫 번째 catch 하나만 실행돼요.

try {
    int[] arr = new int[3];
    arr[5] = 10 / 0;   // 상황에 따라 다른 예외가 날 수 있음
} catch (ArithmeticException e) {
    System.out.println("0으로 나눔 처리");
} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("배열 범위 초과 처리");
} catch (Exception e) {
    // 위에서 못 잡은 나머지 예외를 여기서 폭넓게 처리
    System.out.println("그 밖의 예외 처리");
}
⚠️
순서 주의! 여러 catch를 쓸 때는 좁은(구체적) 예외를 위에, 넓은(상위) 예외를 아래에 둬야 해요. Exception(가장 넓음)을 맨 위에 두면 모든 예외가 거기서 다 잡혀버려서, 아래 구체적인 catch는 영영 실행되지 못해요(컴파일 에러가 나기도 해요).

자바 7부터는 여러 예외를 하나의 catch로 묶는 문법(멀티 캐치, |)도 생겼어요. 처리 방식이 같다면 이렇게 짧게 쓸 수 있어요.

try {
    // ...
} catch (ArithmeticException | ArrayIndexOutOfBoundsException e) {
    // 두 예외를 같은 방식으로 처리
    System.out.println("계산/배열 관련 오류: " + e.getMessage());
}

마지막으로 try-with-resources(자바 7+)예요. 파일이나 연결처럼 다 쓰고 나면 닫아야(close) 하는 자원try(...) 괄호 안에 선언하면, 블록이 끝날 때 자바가 자동으로 close()를 호출해줘요. finally에 일일이 닫기 코드를 쓸 필요가 없어집니다.

// try 괄호 안에 자원을 선언하면 → 끝날 때 자동 close()
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    System.out.println("읽기 실패");
}
// reader.close()는 자바가 알아서 호출해줌 (예외가 나도!)
🆚 finally로 닫기 vs try-with-resources 옛날 방식finally 블록에서 직접 close()를 호출. 코드가 길고, close 자체가 또 예외를 낼 수 있어 번거로웠어요.
try-with-resourcestry(자원선언)만 하면 자동으로 안전하게 닫아줌. 짧고 실수도 줄어요. 💡 정리: 닫아야 하는 자원을 다룰 땐 try-with-resources를 기본으로 쓰는 게 요즘 스타일이에요.
🧠 이 장 핵심 요약
📝
문제풀이 · 점검
배운 걸 가볍게 점검해봐요
시험이 아니라 "내가 이해했나" 확인용이에요. 틀려도 바로 해설이 나와요.
🧪
문제를 풀면 즉시 정답과 해설이 나오고, 위쪽 바에 점수가 쌓여요. 네 가지 유형(객관식 · O/X · 빈칸 · 코드결과)을 섞어 두었어요. 편하게 눌러보세요.
CH 15 📚 컬렉션과 제네릭 — List·Set·Map으로 데이터를 다루고, 제네릭으로 타입을 안전하게