예외 — 컴파일러가 실패를 묻는다
Article
사용자가 입력한 글자를 숫자로 바꾼다.
public class Main {
static int parse(String s) {
return Integer.parseInt(s);
}
static void middle(String s) {
System.out.println(parse(s));
}
public static void main(String[] args) {
middle("abc");
System.out.println("여기까지 왔다");
}
}
- Exception in thread "main" java.lang.NumberFormatException: For input string: "abc"
- at java.base/java.lang.Integer.parseInt(Integer.java:668)
- at Main.parse(Main.java:3)
- at Main.middle(Main.java:7)
- at Main.main(Main.java:11)
여기까지 왔다 는 찍히지 않았다. 프로그램이 그 자리에서 죽었다.
이번 강의에서 두 가지를 한다. 저 글자 뭉치를 읽는 법과, 죽지 않고 붙잡는 법이다.
예외도 객체다
NumberFormatException 은 클래스 이름이다. 문제가 생기면 자바는 그 정보를 담은 객체를 하나 만들어서 던진다.
던진다는 말이 비유가 아니다. throw 라는 키워드가 실제로 있다.
모든 예외 클래스는 하나의 뿌리에서 나온다. 14강의 Object 처럼.
- Throwable
- Error
- StackOverflowError
- OutOfMemoryError
- Exception
- RuntimeException
- NullPointerException
- ArrayIndexOutOfBoundsException
- ArithmeticException
- ClassCastException
- IllegalArgumentException
- IOException
- RuntimeException
- Error
지금까지 만난 예외들이 전부 한 자리에 모였다. 그리고 대부분 RuntimeException 아래에 있다.
Error 는 다르다. JVM 자체에 문제가 생겼다는 뜻이라, 우리가 붙잡아도 할 수 있는 게 없다. 10강의 StackOverflowError 가 Exception 이 아니라 Error 인 이유다.
스택 트레이스를 읽는다
에러 메시지의 at ... 줄들을 스택 트레이스라고 부른다. 10강의 콜 스택을 그대로 찍어낸 것이다.
- Integer.parseInt()
- parse()
- middle()
- main()
Call Stack
출력은 위가 터진 곳, 아래가 시작한 곳이다. 스택을 위에서부터 훑어 내려온 것이다.
- 01
첫 줄에서 무엇이 · 왜
NumberFormatException. For input string: "abc" — "abc" 를 숫자로 못 바꾸겠다.
- 02
내 코드가 처음 나오는 줄을 찾는다
java.base 로 시작하는 줄은 자바 내부다.
at Main.parse(Main.java:3)이 첫 단서다. - 03
아래로 내려가며 경로를 확인한다
main 이 middle 을 부르고, middle 이 parse 를 불렀다. 어디서 온 값인지 거슬러 올라간다.
가장 위가 아니라 내 코드가 처음 나오는 줄을 본다. 자바 내부 코드는 대개 잘못이 없다.
붙잡는다
try 안에서 터진 예외를 catch 가 받는다.
try {
middle("abc");
} catch (NumberFormatException e) {
System.out.println("잡았다: " + e.getMessage());
}
System.out.println("여기까지 왔다");
- 잡았다: For input string: "abc"
- 여기까지 왔다
프로그램이 살아남았다.
e 는 던져진 그 객체다. e.getMessage() 로 메시지를, e.printStackTrace() 로 스택 트레이스를 꺼낼 수 있다.
finally 는 예외가 나든 안 나든 실행된다.
try {
return "try";
} finally {
System.out.println("finally 먼저");
}
- finally 먼저
- try
return 을 만나도 finally 를 거쳐 간다. 그래서 정리 작업을 여기에 둔다. 파일을 닫거나 연결을 끊는 일이다.
catch 는 부모로도 받는다
계보에서 위에 있는 것으로 받으면 아래 것들이 전부 걸린다.
try {
throw new IllegalStateException("직접 던짐");
} catch (RuntimeException e) {
System.out.println("부모로 잡힌다: " + e.getMessage());
}
IllegalStateException 은 RuntimeException 의 자식이다. 14강의 다형성이 여기서도 일한다.
그래서 순서가 중요하다. 부모를 위에 두면 자식은 영원히 못 잡는다.
catch (RuntimeException e) { }
catch (IllegalArgumentException e) { }
- error: exception IllegalArgumentException has already been caught
컴파일러가 막아준다. 좁은 것을 위에, 넓은 것을 아래에. 7강의 else if 와 같은 규칙이다.
둘을 똑같이 처리할 거면 세로줄로 묶는다.
catch (ArrayIndexOutOfBoundsException | NullPointerException e) { ... }
컴파일러가 묻는 예외, 안 묻는 예외
파일을 읽어보자.
FileReader r = new FileReader("x.txt");
- error: unreported exception FileNotFoundException;
- must be caught or declared to be thrown
실행도 안 했는데 컴파일 에러다. "붙잡든지, 던진다고 선언하든지 하라"고 한다.
반면 이건 컴파일이 잘 된다.
String s = null;
s.length(); // 컴파일 통과. 실행하면 NPE
이 차이가 자바 예외의 가장 큰 특징이다.
checked 예외
Exception 의 자식 (RuntimeException 제외)
- 컴파일러가 처리했는지 묻는다
IOException,FileNotFoundException- 파일·네트워크처럼 내 잘못이 아닌 실패
- 안 다루면 컴파일이 안 된다
unchecked 예외
RuntimeException 의 자식
- 컴파일러가 묻지 않는다
NullPointerException,ArithmeticException- 대개 코드가 잘못돼서 나는 실패
- 붙잡기보다 안 나게 고친다
checked 예외를 처리하는 방법은 둘이다. 여기서 붙잡거나, 부른 쪽에게 떠넘기거나.
static void read() throws IOException {
FileReader r = new FileReader("x.txt");
}
throws 는 "나는 이 예외를 낼 수 있으니 알아서 처리하라"는 선언이다. throw(던진다)와 글자 하나 차이인데 뜻이 다르다.
떠넘기면 부른 쪽이 같은 고민을 한다. main 까지 올라가면 프로그램이 죽는다.
직접 던진다
12강에서 이미 해봤다.
public void setPrice(int price) {
if (price < 0) {
throw new IllegalArgumentException("가격은 음수일 수 없다: " + price);
}
this.price = price;
}
IllegalArgumentException 은 RuntimeException 의 자식이라 throws 를 안 적어도 된다. 잘못된 값을 넘긴 쪽의 코드가 틀린 것이니 unchecked 가 맞다.
내 예외를 만들 수도 있다. 상속만 하면 된다.
class BookNotFoundException extends RuntimeException {
public BookNotFoundException(String title) {
super(title + " 을(를) 찾을 수 없다");
}
}
super(...) 로 부모에게 메시지를 넘긴다. 14강의 그 super 다.
이름만 봐도 무슨 일인지 아는 예외를 만드는 것. 그게 예외를 쓰는 이유다. null 을 돌려주면 부르는 쪽은 아무것도 모른다.
삼키지 않는다
가장 나쁜 코드는 이것이다.
try {
중요한작업();
} catch (Exception e) {
// 아무것도 안 함
}
catch (Exception e) 처럼 넓게 잡는 것도 조심한다. 예상한 예외만 잡는 게 낫다.
자원은 자동으로 닫는다
파일이나 연결은 다 쓰면 닫아야 한다. finally 에 적으면 되지만 길어진다.
try 옆의 괄호 안에서 만들면 자바가 알아서 닫아준다.
try (FileReader r = new FileReader("x.txt")) {
// 읽는다
} catch (IOException e) {
System.out.println("못 읽었다");
}
블록을 벗어날 때 r.close() 가 자동으로 불린다. 예외가 터져도 불린다. 이걸 try-with-resources 라고 한다.
정리하면
예외는 객체다. 컴파일러는 checked 만 묻고, unchecked 는 대개 코드를 고쳐야 할 신호다.
- 01
스택 트레이스는 아래에서 위로 흘렀다
위가 터진 곳. 내 코드가 처음 나오는 줄부터 본다.
- 02
try·catch·finallyfinally 는 return 을 만나도 실행된다. 정리 작업을 둔다.
- 03
좁은 catch 를 위에 둔다
부모로 먼저 잡으면 자식은 영원히 안 잡힌다. 컴파일러가 막아준다.
- 04
checked 는 붙잡거나
throws로 떠넘긴다unchecked 는 묻지 않는다. 대개 코드가 잘못된 것이라 고치는 게 맞다.
static void read() throws IOException { ... } - 05
예외를 삼키지 않는다
빈 catch 블록은 실패를 없었던 일로 만든다. 죽는 게 낫다.
예외는 실패를 사건으로 다루는 방법이다. 값을 돌려주는 대신 흐름을 끊고 위로 던진다.
그런데 실패가 아니라 "값이 없음"이라면 어떨까. 16강에서 map.get() 은 없는 키에 null 을 돌려줬다. 예외를 던지지도 않았다. 그 null 은 한참 뒤에 엉뚱한 곳에서 터진다.
20강에서 자바가 준비한 답을 본다. 그전에 19강에서, 자바를 아주 많이 바꿔놓은 문법 두 개를 먼저 배운다. 화살표(->)와 점 세 개로 이어지는 체인이다.