해당 글에서는 Java에서 지원하는 예외처리 방식에 대해 살펴볼 것이다.
Java에서는 예상치 못한 상황을 처리하고자 에러 처리에 대한 공통 조상인Throwable
클래스를 제공한다.
따라서, Throwable
타입을 상속받은 구체화된 객체들은 특정한 예외적인 상황을 의미하고 특정 상황에 맞는 기능들을 제공한다.
아래 그림을 보다시피, 예상치 못한 상황에 대해서 크게 Error
, Exception
타입으로 구분하고 있다.
그럼, Error
와 Exception
타입은 무엇을 의미하고 차이점이 무엇인지 살펴보자.
Error
는 JVM 같이 시스템 레벨에서 비정상적인 상황이 생겼을 때 발생한다.
즉, 애플리케이션 레벨에서는 처리할 수 없는 예상치 못한 상황인 것이다.
모든 Java 애플리케이션은 JVM 위에서 구동되는데, JVM 단에서 발생하는 에러를 개발 단계에서 예상할 수 없을 뿐만 아니라 애플리케이션에서 제어할 방법이 없다.
따라서, 시스템 레벨의 문제를 애플리케이션 레벨에서 처리할 수 없기 때문에 Error
발생시 프로그램이 종료되는 것은 자연스러운 흐름이다.
결과적으로 Error
는 JVM 같은 시스템 레벨에서 발생하는 에러를 표현하기 위한 타입이고, 시스템에 변화를 주어 문제를 처리해야 하는 경우가 일반적이다.
시스템 레벨의 에러는 Error
타입으로 나타내는 것을 알았다.
그럼 애플리케이션에서 발생하는 에러는 어떻게 표현할까?
맞다. Exception
이다.
Exception
은 애플리케이션에서 핸들링 가능한 예외를 의미하는데, 내부적으로 Checked Exception
, Unchecked Exception
으로 구분한다.
아래 그림을 보면 알 수 있듯이, Unchecked Exception
은 RuntimeException
으로 나타내고 RuntimeException
아닌 예외는 Checked Exception
이다.
그럼 Checked Exception
, Unchecked Exception
차이점은 무엇일까?
Checked Exception
, Unchecked Exception
차이점은 위와 같이 크게 네 가지로 구분하는데, 가장 기본적인 구분 기준은 예외 처리의 강제성 여부이다.
Checked Exception
은 API 사용자측에게 반드시 예외 처리를 하도록 강제한다.
즉, Checked Exception
는 애플리케이션 종료시키지 않고 복구 가능한 예외라는 것을 의미한다.
예를 들어, 만약 개발 과정에서 라이브러리 사용하는데 특정 함수에서 Checked Exception
을 던진다고 가정해보자.
라이브러리는 해당 함수 실행 과정에서 발생 가능한 예외를 반드시 호출자(사용자)측에서 처리하도록 강제하는데, 이를 통해서 호출자는 특정한 상황에 대한 예외를 복구할 수 있는 기회를 얻게 되는 것이다.
뿐만 아니라, 함수에서 Checked Exception
을 던졌다는 것은 해당 예외에 대한 명확한 복구 방법을 라이브러리에서 제공하고 있다는 것을 반증하기도 한다.
public class Test {
public void testCheckedException() {
try {
File file = new File("/wrong/path", "test.txt");
file.createNewFile();
} catch (IOException e) {
// handle Checked Exceptions
} finally {
// return resources
}
}
}
그래서 만약 자신이 함수 구현하고 있고 Checked Exception
을 던져야 하는 상황이라면, 반드시 해당 예외에 대한 복구 방법도 동반해서 제공하는 것을 권장한다.
실제로 일반적으로 Checked Exception
예외가 발생한 경우, 그에 맞는 복구 전략으로 예외를 복구할 수 있는 경우는 그렇게 많지 않다.
현재 특정 함수로 부터 전달받은 Checked Exception
을 복구할 방법이 정말 없다면, 해당 Checked Exception
을 Unchecked Exception
으로 변경해서 예외를 상위로 전달하도록 하자.
catch(SQLException e) {
...
throw DuplicateUserIdException();
}
이러한 예외 전환을 통해서 다른 계층에서 일일이 예외를 선언해서 처리할 필요가 없도록 할 수 있다.
만약 Checked Exception
예외에 대한 복구 전략이 명확하다면, try ~ catch
구문으로 복구를 수행하면 된다.
반대로 복구 전략이 명확하지 않다면, 명확한 메시지를 담은 좀 더 구체적인 Unchecked Exception
타입으로 전환하는 것이 효과적이다.
Unchecked Exception
은 API 사용자측에게 반드시 예외 처리를 하도록 강제하지 않는다.
즉 호출자 측에서는 Unchecked Exception
예외를 처리해도 되고 안해도 그만이다.
구체적으로 Unchecked Exception
는 호출자 측에서 해당 예외에 대한 복구 방법이 없다면, 프로그램을 종료시키겠다는 의미를 가진다.
예를 들어, 만약 개발 과정에서 라이브러리 사용하는데 특정 함수에서 Unchecked Exception
을 던진다고 가정해보자.
라이브러리는 해당 함수 실행 과정에서 발생한 예외를 호출자(사용자)측에서 복구할 방법이 있다면 처리하고, 복구 할 수 없다면 상위 호출자에게 예외를 넘기고 최종적으로도 복구 불가능하다면 최후의 방법으로 프로그램을 종료시키겠다는 것이다.
즉, 최종 호출자까지도 복구할 방법을 찾지 못한다면 크리티컬한 예외로서 프로그램을 종료시키는 것이 났다고 판단하는 것이다.
Unchecked Exception
은 RuntimeException
또는 하위 객체들이다.
아래 예제는 Integer.parseInt()
인자에 0~9
가 아닌 문자를 포함한 경우이다.
public class Test {
public void testUncheckedException() {
Integer value = Integer.parseInt("12A");
System.out.println(value);
}
}
0~9
가 아닌 문자를 포함하게 되면, Integer.parseInt()
는 Unchecked Exception
인 NumberFormatException
를 던진다.
만약 testUncheckedException()
에서도 복구가 불가능하다면, NumberFormatException
예외는 상위 호출자(testUncheckedException()
메서드 호출자)에게 전달되고 최종 호출자도 처리할 수 없다면 프로그램은 다음과 같이 종료된다.
하지만, testUncheckedException()
에서도 복구 가능하다면, NumberFormatException
예외는 testUncheckedException()
메서드에서 처리되기 때문에 상위 호출자(testUncheckedException()
메서드 호출자)에게 전달되지 않는다.
public class Test {
public void testUncheckedException() {
Integer value;
try {
value = Integer.parseInt("12A");
} catch(NumberFormatException exception) {
value = Integer.parseInt("123");
}
System.out.println(value);
}
}