크게 둘로 나뉜다.
try {
//예외가 발생할 수 있는 코드
} catch(EXCEPTION_NAME e) {
//예외 처리
} finally {
//예외 발생 유무에 관계 없이 무조건 실행
//생략 가능
}
예를 들어 NumberFormatException
은 다음과 같이 처리할 수 있다.
String str = "Three";
try {
int num = Integer.parseInt(str);
System.out.println("Result: " + num); //변환 결과 출력
} catch(NumberFormatException e) {
System.out.println("Failure"); //예외 발생 시 실패 메시지 출력
} finally {
System.out.println("String: " + str); //원본 문자열은 무조건 출력
}
위와 같은 형식을 try-catch-finally라 한다.
주의할 점은 try
블록이나 catch
블록에서 return
문을 쓰더라도 finally
블록은 실행된다는 것이다. 말 그대로 끝내는 실행이 되는 것이다.
이런 특성을 활용해 리소스 처리에 응용할 수도 있는데, 이건 여기를 참고하기 바란다.
try {
//예외가 발생할 수 있는 코드
} catch(EXCEPTION1 e1) {
//예외 처리1
} catch(EXCEPTION2 e2) {
//예외 처리2
} finally {
}
이 경우 예외를 처리할 catch
블록은 위에서부터 차례대로 검색되기 때문에 상위 예외 처리 코드를 아래에 적어야 한다. 다시 말해 다음은 바람직하지 않다.
try {
//...
} catch(Exception e) {
System.out.println("Unknown exception");
} catch(NullPointerException e) {
System.out.println("NullPointerException");
} finally {
}
NullPointerException
클래스는 java.lang.Exception
클래스를 상속한다.
따라서 만약 Exception
에 대한 catch 블록을 먼저 작성한다면 NullPointerException
이 발생하더라도 Exception
의 처리 코드가 실행되므로 NullPointerException
에 대한 catch 블록은 무용지물이 된다.
애초에 Unreachable catch block
이라고 컴파일 에러 뜸.
try {
} catch(EXCEPTION1 | EXCEPTION2 | ... | EXCEPTIONn e) {
} finally {
}
다음과 같이 메소드를 호출한 곳으로 예외를 떠넘길 수도 있다.
RETURN_TYPE1 method1(param1, param2, ...) throws EXCEPTION1, EXCEPTION2, ... {
...
}
RETURN_TYPE2 method2(param1, param2, ...) throws Exception {
...
}
throws
키워드는 호출한 곳으로 예외를 떠넘긴다는 의미를 갖는다.
때문에 이 메소드는 반드시 try
블록에서 호출되어야 한다. try
블록에서 호출되어 예외가 발생하면 catch
블록에서 그 예외를 받아 처리하는 것이다. 키워드의 의미로 접근하면 다음과 같다.
throws
: 예외가 발생하면 떠넘긴다(throws
).try
: 예외가 발생하는지 발생하지 않는지 시도(try
)한다.catch
: try
블록에서 떠넘긴 예외를 받아(catch
) 처리한다.public void method1() throws Exception {
...
}
예를 들어 이런 메소드를 다른 메소드에서 호출하려면
다음과 같이 호출해야 한다.
public void method2() {
try {
method1();
} catch(Exception e) {
}
}
main()
에서 예외 떠넘기기main()
에서도 throws
키워드를 사용해 예외를 떠넘길 수 있다. 이 경우 JVM이 예외를 처리하게 되는데, JVM은 예외를 모니터에 출력함으로써 처리한다.
그런데 사용자의 입장에서는 프로그램을 잘 사용하다가 갑자기 알 수 없는 내용을 출력하고 프로그램이 종료되는 것이므로 바람직한 방법이라고 할 수 없다.
그래서 main()
에서는 throw하지 않고 try-catch
블록으로 처리하는 것이 바람직하다.
method에서도 예외를 떠넘길 수 있고, 생성자에서도 떠넘길 수 있다.
여기서 떠넘긴 예외는 호출한 쪽이 받아야 한다.
A가 B를 호출하고, B가 C를 호출하고 있다고 하자.
여기서 B와 C는 메소드든 생성자든 상관없다.
C 내부에서 발생한 예외는 B가 처리할 수도 있고 떠넘길 수도 있다.
B가 처리하지 않고 떠넘긴다면 A에서 처리해야 한다.
A에서 떠넘긴다면 A를 호출한 곳(main
메소드든 뭐든)에서 처리해야 한다.
계속 떠넘기다가 main
메소드에서마저 떠넘기면 JVM이 처리한다.
근데 이게 말이 처리지 사실상 프로그램이 지 혼자 숨는 거라 main
메소드에서는 절대 throw
하지 않는다.
정확히는 그냥 이상한 에러 메시지 띄우고 종료돼버린다.
아무튼 예외를 떠넘기는 일련의 과정을 Exception chaining이라 한다.
Exception chaining에 관한 내용은 여기를 참고할 것.
모든 예외 클래스는 다음의 두 메소드를 갖는다.
getMessage()
String
)를 얻는 메소드.printStackTrace()
getMessage()
String
객체로 반환하는 메소드.public void method2() {
try {
method1();
} catch(Exception e) {
System.out.println(e.getMessage());
}
}
printStackTrace()
printStackTrace
메소드는 예외 발생 경로를 추적해 출력하는 메소드다.printStackTrace()
메소드는 다음과 같이 한 라인에 하나의 프레임을 표현한다.
public class Example {
public static void main(String[] args) {
String str = "Three";
try {
int num = Integer.parseInt(str);
System.out.println("변환 결과: " + num);
} catch(NumberFormatException e) {
e.printStackTrace();
}
}
}
위의 출력 결과가 스택 트레이스다. 대략적인 내용은 다음과 같다.
Example.java의 12번째 줄에서 입력된 문자열 "Three"에 대해 NumberFormatException이 발생했다.
스택이기 때문에 출력 결과에서 파란색 부분을 클릭하면 해당 위치로 이동한다.
예를 들어 NumberFormatException.java:65
를 클릭하면 NumberFormatException.java
의 65번째 줄로 이동한다.
이제 위에서부터 차례대로 읽어보며 원인을 파악하면 된다.
우선 맨 윗줄을 읽어 무슨 예외가 발생했는 지 파악한다. 그리고 아래의 내용을 읽는다.
NumberFormatException.java
의 65
번째 줄이 실행되어 NumberFormatException
이 발생했고,
NumberFormatException.java
의 65
번째 줄은 Integer.java
의 580
번째 줄에 의해 실행되었고,
Integer.java
의 580번째 줄은 Integer.java
의 615번째 줄에 의해 실행되었고,
Integer.java
의 615번째 줄은 Example.java
의 12번째 줄에 의해 실행되었음을 알 수 있다.
요약하면 다음과 같다.
Integer 클래스의 parseInt() 내부(Integer 클래스 615번째 줄)에서 같은 클래스의 overload된 parseInt()를 호출했는데,
그 메소드 내부(Integer 클래스 580번째 줄)에서 NumberFormatExceptinon이 발생했다.
이는 문자열 "Three"가 주어졌기 때문이다.
주의할 점은 '스택'이기 때문에 순서를 반대로 생각해야 한다는 것이다. 즉 호출 순서로 따지면 아래에서 위로 호출해나간 것이다.
다시 말해 가장 아래부분이 최초의 호출 부분이므로, 거기부터 위로 타고 들어감으로써 예외가 발생한 경위를 파악할 수 있다.
예시에서는 스택 트레이스가 간단하기 때문에 읽어내는 데에 큰 어려움이 없지만, 규모가 큰 프로그램에서는 스택 트레이스가 수십 줄이 넘을 수도 있다. 때문에 스택 트레이스 해석에 익숙해지는 것이 좋다.
이 부분에 관해서는 여기를 참고하면 좋을 것이다.