예외란 무엇인가
예외는 자바가 "무언가 잘못되어 정상적으로 계속할 수 없다"고 말하는 방식입니다. 잘못된 값을 반환하거나 데이터를 조용히 손상시키는 대신, 런타임은 문제를 설명하는 예외 객체를 생성하고 그것을 던집니다. 정상적인 실행은 그 지점에서 멈추고, 자바는 그 상황을 처리하는 방법을 아는 코드를 찾기 시작합니다.
아무도 그것을 처리하지 않으면 예외는 프로그램의 최상위까지 도달하고, JVM은 스택 트레이스를 출력하며, 프로세스는 0이 아닌 상태로 종료됩니다.
"after"가 결코 출력되지 않는다는 점에 주목하세요. numbers[5]가 평가되는 순간 ArrayIndexOutOfBoundsException이 던져지고 main의 나머지 부분은 버려집니다.
스택 트레이스 읽기
예외가 처리되지 않은 채 남으면 다음과 같은 출력을 얻게 됩니다. 위압적으로 보이지만 사실은 그저 하나의 목록일 뿐입니다.
Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.divide(Main.java:8)
at Main.main(Main.java:4)
위에서 아래로 읽습니다.
- 첫 번째 줄은 타입(
ArithmeticException)과 메시지(/ by zero)입니다. - 각
at ...줄은 스택 프레임입니다. 맨 위는 예외가 실제로 던져진 곳, 즉 8번째 줄의divide입니다. 그 아래는 호출자, 4번째 줄의main입니다.
맨 위의 at 줄이 가장 먼저 살펴볼 곳입니다. 그 아래의 프레임들은 "어떻게 여기까지 왔는가?"라는 질문에 답합니다.
이것을 실행하면 트레이스는 곧장 a / b 줄을 가리키고, 이어서 호출자로 main을 보여줍니다. 이 호출 사슬은 자바가 무료로 제공하는 가장 유용한 디버깅 도구입니다.
예외 계층 구조
모든 예외는 객체이며, 그들은 모두 Throwable에서 파생됩니다. 중요한 두 갈래는 다음과 같습니다.
Error—OutOfMemoryError나StackOverflowError처럼 JVM이 일으키는 심각한 문제입니다. 일반적으로 이것들은 잡지 않습니다.Exception— 프로그램이 합리적으로 예상하고 처리할 수 있는 문제입니다. 그 안에RuntimeException이 있으며, 이는NullPointerException같은 일상적인 버그의 부모입니다.
Throwable
├── Error (don't catch: OutOfMemoryError, StackOverflowError)
└── Exception
├── IOException (checked)
├── SQLException (checked)
└── RuntimeException (unchecked)
├── NullPointerException
├── ArithmeticException
└── ArrayIndexOutOfBoundsException
이것들이 실제 클래스이기 때문에, 예외는 메시지를 가질 수 있고 자기 자신에 대해 물어볼 수 있습니다.
getMessage()는 스택 트레이스에서 콜론 뒤의 텍스트를 반환하고, getClass().getSimpleName()은 예외 타입을 이름으로 알려줍니다.
검사 예외와 비검사 예외
이것은 초보자가 가장 많이 헷갈려 하는 구분입니다.
- 비검사 예외는
RuntimeException을 확장합니다. 보통 코드의 버그를 의미합니다. 예상하지 못한null, 잘못된 인덱스, 0으로 나누기 같은 것들이죠. 컴파일러는 이를 처리하도록 강제하지 않습니다. - 검사 예외는
Exception을 확장하지만RuntimeException은 확장하지 않습니다(예:IOException). 이것들은 여러분의 통제를 벗어난 상황을 나타냅니다. 없는 파일, 끊어진 네트워크 연결 같은 것들이죠. 컴파일러는 이것들을 잡거나 선언하도록 강제합니다.
메서드가 검사 예외를 던질 수 있다면 throws로 그것을 명시해야 하며, 모든 호출자가 그것을 처리해야 합니다.
main에서 throws Exception을 제거하면 코드는 아예 컴파일되지 않습니다. 이것이 바로 컴파일러가 검사 예외 계약을 강제하는 것입니다. 비검사 예외는 결코 이를 요구하지 않습니다.
직접 예외 던지기
예외에 반응만 하는 것이 아닙니다. 직접 일으킬 수도 있습니다. 인수나 상태가 유효하지 않음을 알리려면 새 예외 객체와 함께 throw를 사용하세요. 이것은 -1 같은 매직 값을 반환하고 호출자가 그것을 확인해 주기를 바라는 것보다 훨씬 낫습니다.
무엇이 잘못되었는지, 그리고 이상적으로는 문제가 된 값을 설명하는 메시지를 항상 포함하세요. 스택 트레이스를 읽을 미래의 여러분이 고마워할 것입니다. 입력을 검증할 때 가장 자주 꺼내 쓰게 될 두 가지는 IllegalArgumentException과 IllegalStateException입니다.
흔한 함정
- 예외 삼키기. 예외를 잡고 아무것도 하지 않으면(빈 블록) 버그가 숨겨집니다. 최소한 로그로 남기세요. 보통은 처리하거나 다시 던져야 합니다.
Exception을 너무 넓게 잡기. 기반Exception(또는 더 나쁘게Throwable)을 잡으면 처리할 의도가 없던 문제까지 가릴 수 있습니다. 예상하는 구체적인 타입을 잡으세요.- 트레이스를 아래에서 위로 읽기. 가장 관련 있는 프레임은 맨 위의
at줄이지, 맨 아래가 아닙니다. 거기서 시작하세요. Error와Exception혼동하기.OutOfMemoryError나StackOverflowError에서 복구하려고 하지 마세요. 대신 근본 원인을 고치세요.
다음: try-catch
이제 예외가 무엇인지, 그리고 그것을 읽는 방법을 알게 되었습니다. 다음 페이지에서는 그것들을 실제로 처리하는 방법을 다룹니다. 위험한 코드를 try 블록으로 감싸고, catch에서 복구하며, finally에서 정리 코드를 실행하여 프로그램이 충돌하는 대신 계속 동작하도록 하는 것입니다.
자주 묻는 질문
자바에서 예외란 무엇인가요?
예외는 프로그램이 실행되는 동안 감지된 문제를 나타내는 객체입니다. 예를 들어 0으로 나누기, 배열의 끝을 넘어선 인덱스 접근, null에 대한 메서드 호출 등이 있습니다. 문제가 발생하면 자바는 예외를 던집니다. 즉, 정상적인 흐름을 멈추고 그것을 처리할 수 있는 코드를 찾습니다. 아무것도 처리하지 않으면 프로그램은 스택 트레이스를 출력하고 종료됩니다.
자바에서 검사 예외와 비검사 예외의 차이는 무엇인가요?
검사 예외(Exception의 하위 클래스이지만 RuntimeException은 아닌 것, 예: IOException)는 반드시 잡거나 throws로 선언해야 하며, 컴파일러가 이를 강제합니다. 비검사 예외(RuntimeException의 하위 클래스, 예: NullPointerException, ArrayIndexOutOfBoundsException)는 보통 프로그래밍 버그를 나타내며 선언이 필요 없습니다. Error(OutOfMemoryError 등)도 비검사이며 일반적으로 잡도록 의도되지 않습니다.
자바 스택 트레이스는 어떻게 읽나요?
위에서 아래로 읽습니다. 첫 번째 줄은 예외의 타입과 메시지를 나타냅니다(예: java.lang.ArithmeticException: / by zero). 그 아래의 각 at ... 줄은 메서드, 파일, 줄 번호를 보여주는 스택 프레임이며, 예외가 던져진 곳에서 시작해 호출자를 거슬러 올라갑니다. 맨 위의 at 줄은 거의 항상 가장 먼저 살펴봐야 할 곳입니다.