아무리 잘 짠 프로그램도 실행 중에 예상 못 한 일이 터져요. 파일이 없거나, 0으로 나누거나, 없는 값을 건드리거나. 이번 장에서는 그런 상황에서 프로그램이 뻗지 않고 우아하게 대응하는 예외 처리(Exception Handling)를 단계별로 배웁니다.
예외(Exception)는 프로그램이 실행되는 도중(런타임, runtime)에 발생하는 오류예요. 문법은 멀쩡해서 컴파일도 잘 됐고 실행도 시작됐는데, 돌아가는 중에 예상치 못한 일이 생겨 정상 흐름이 막히는 상황이죠. 예를 들어 "있지도 않은 파일을 열려고 하거나", "숫자를 0으로 나누거나", "비어 있는(null) 값을 건드리는" 경우예요.
그럼 왜 예외를 "처리"해야 할까요? 예외가 발생했는데 아무 대응도 없으면, 프로그램은 그 자리에서 비정상 종료(뻗어요)돼요. 은행 앱이 잔액 조회 한 번 실패했다고 통째로 꺼지면 곤란하겠죠. 예외 처리는 "문제가 생겨도 프로그램이 죽지 않고, 사용자에게 우아하게 안내하거나 대체 동작을 하도록" 만드는 안전장치예요.
catch)로 빠져요.OutOfMemoryError)처럼 시스템 수준의 고장이에요. 보통 잡아서 처리하지 않아요(잡아도 딱히 할 수 있는 게 없음).try-catch로 잡아서 처리하는 대상은 주로 예외(Exception)예요. 에러(Error)는 잡는 대상이 아니라고 생각하면 편해요.
예외 처리의 기본은 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 / 0은 0으로 나누는 잘못된 연산이라 ArithmeticException이 발생해요. 그 순간 try 안의 나머지 줄은 건너뛰고 곧바로 catch로 점프합니다. 중요한 건, 예외를 잡았기 때문에 프로그램이 죽지 않고 마지막 줄까지 계속 실행된다는 거예요.
try 진입 → 10 / 0 실행 시도ArithmeticException 발생 → println(result)는 건너뜀catch로 점프 → "0으로 나눌 수 없어요!" 출력또 다른 흔한 예로 배열의 범위를 벗어난 접근이 있어요. 길이가 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()); // 예외가 담고 있는 정보
}
e는 예외 객체예요. 예외가 발생하면 자바가 그 정보를 담은 객체를 만들어 catch (예외타입 e)의 e로 넘겨줘요. e.getMessage()로 메시지를, e.printStackTrace()로 어디서 터졌는지 자세히 볼 수 있어요.finally 블록은 예외 발생 여부와 상관없이 항상 실행되는 마무리 코드예요. try가 무사히 끝났든, 중간에 예외가 터져 catch로 넘어갔든, 마지막에는 반드시 finally를 거쳐요. 그래서 자원 정리(파일 닫기, 연결 끊기 등)처럼 "어떤 경우든 꼭 해야 하는 일"에 씁니다.
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는 항상 실행돼요.
| 상황 | try | catch | finally |
|---|---|---|---|
| 예외 안 남 | ✅ 전부 실행 | ⛔ 건너뜀 | ✅ 실행 |
| 예외 발생 | ⚠️ 예외 지점까지만 | ✅ 실행 | ✅ 실행 |
자바의 모든 예외와 에러는 Throwable이라는 최상위 클래스에서 갈라져 나와요. 이 Throwable 아래로 크게 두 갈래, Error와 Exception이 있어요.
여기서 중요한 건 Exception 아래를 다시 두 종류로 나눈다는 점이에요. RuntimeException 계열이냐 아니냐로 갈립니다.
RuntimeException이 아닌 Exception들. 컴파일러가 "이거 처리 안 하면 컴파일 안 해줄 거야"라고 강제해요. 그래서 반드시 try-catch로 잡거나 throws로 떠넘겨야 컴파일이 돼요. 대표 예: 파일 입출력의 IOException.RuntimeException과 그 자식들. 컴파일러가 처리를 강제하지 않아요. 주로 프로그래머의 실수로 생기는 것들이라, 안 잡아도 컴파일은 됩니다(대신 실행 중에 터짐). 대표 예: NullPointerException, ArithmeticException, ArrayIndexOutOfBoundsException.
💡 외우는 요령: "Runtime이면 unchecked". RuntimeException 밑에 있으면 unchecked, 아니면 checked예요.
| 구분 | checked 예외 | unchecked 예외 |
|---|---|---|
| 뿌리 | Exception (RuntimeException 제외) | RuntimeException 계열 |
| 컴파일러 처리 강제 | ✅ 반드시 처리해야 컴파일됨 | ⛔ 강제하지 않음 |
| 주로 생기는 이유 | 외부 요인(파일 없음, 네트워크 등) | 프로그래머의 실수(로직 오류) |
| 대표 예 | IOException | NullPointerExceptionArithmeticExceptionArrayIndexOutOfBoundsException |
null인 변수를 마치 객체인 것처럼 사용(메서드 호출·필드 접근)하려 할 때 발생해요. 예: String s = null; s.length(); → 없는 걸 건드려서 💥. 자바에서 가장 흔한 예외로 꼽혀요.
2. ArithmeticException
정수를 0으로 나눌 때 발생해요. 예: 10 / 0. (참고로 실수 10.0 / 0은 예외 대신 Infinity가 나와요.)
3. ArrayIndexOutOfBoundsException
배열에서 존재하지 않는 인덱스에 접근할 때 발생해요. 길이 3인 배열의 arr[5] 같은 경우죠. 셋 다 RuntimeException(unchecked)이라는 공통점이 있어요.
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 new IllegalArgumentException(...);void m() throws IOException
💡 헷갈리면: throw = 던진다(행동), throws = 던질 수 있다고 알린다(선언). s 하나 차이지만 하는 일이 완전히 달라요.
IOException 같은 checked 예외는 반드시 try-catch로 잡거나 throws로 넘겨야 컴파일돼요. "여기서 처리하기 애매하다" 싶으면 throws로 넘기고, 최종적으로 책임질 수 있는 곳에서 try-catch로 잡는 게 흔한 패턴이에요.한 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("그 밖의 예외 처리");
}
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 블록에서 직접 close()를 호출. 코드가 길고, close 자체가 또 예외를 낼 수 있어 번거로웠어요.try(자원선언)만 하면 자동으로 안전하게 닫아줌. 짧고 실수도 줄어요.
💡 정리: 닫아야 하는 자원을 다룰 땐 try-with-resources를 기본으로 쓰는 게 요즘 스타일이에요.
try에, 예외가 나면 catch로 점프해 처리. 예외 발생 시 try의 나머지는 건너뛰어요.Throwable → Error / Exception. Exception 아래에 RuntimeException(unchecked)이 있어요.IOException)는 컴파일러가 처리를 강제, unchecked(예: NullPointerException·ArithmeticException·ArrayIndexOutOfBoundsException)는 강제하지 않아요.throw new ...), throws는 메서드가 예외를 호출자에게 떠넘기는 선언이에요.