https://radio-weblogs.com/0122027/stories/2003/04/01/JavasCheckedExceptionsWereAMistake.html
Java의 Checked Exception은 실수다?
프로그램의 실행/컴파일 단계에서 많은 에러가 발생하게 된다. Java에서는 Error와 Exception이 있으며, Exception의 경우 checked exception
과 unchecked exception
으로 분류 된다.
Error와 Exception 모두 어떠한 이유에 의해 프로그램이 정상적으로 작동하지 않은 경우에 발생하며, 발생한 문제의 심각성에 따라 둘을 구분한다.
Error는 프로그램이 종료되어야 할, 복구할 수 없는 심각한 문제에 해당하며, stack overflow나 memory 부족 등의 경우가 그 예시이다. Java의 Error 예시로 java.lang
의 OutOfMemoryError
가 있다.
다음으로 Exception은 문제가 발생하였지만, 프로그램 수준에서 복구할 수 있는 경우에 해당한다. 필요한 파일을 찾지 못한 FileNotFoundException
이나, 수학 연산 중 발생하는 division by zero와 같은 ArithmeticException
이 그 예시이다.
Java에서 모든것은 class이다. 그렇기에 Error와 Exception 또한 class로서 구현이 되어있다. Error와 Exception은 모두 Throwable
을 상속받으며, Throwable
class는 모든 java class의 상위 class인 Object
class를 상속하고 있다. Error와 Exception의 상속 구조는 아래와 같다
Exception과 Error의 상위 class인 Throwable
에 대한 공식 문서를 보면
The Throwable class is the superclass of all errors and exceptions in the Java language. Only objects that are instances of this class (or one of its subclasses) are thrown by the Java Virtual Machine or can be thrown by the Java throw statement.
위와 같이 JVM이나 Java 언어를 통해 발생할 수 있는 error나 exception은 이 class를 상속해야 함을 설명하고 있으며,
A throwable contains a snapshot of the execution stack of its thread at the time it was created. It can also contain a message string that gives more information about the error. ...
이와 같이 해당 error/exception이 발생한 상황에 대한 정보를 포함함을 알 수 있다. Throwable class의 대표적인 method로는 getMessage()
, getCause()
, printStackTrace()
등이 있다.
Java의 Exception에는 두 종류가 있다. 바로 checked exception과 unchecked exception이다. Checked exception은 compile 단계에서 해당 exception에 대한 handling이 되어 있는지 확인하고, 그렇지 않은 경우에 compile에 실패하는, 개발자에 의해 꼭 처리 되어야 하는 exception이다. 반대로 unchecked exception은 해당 exception에 대한 handling이 되어 있지 않더라도 compile이 되며, RuntimeException
을 상속하는 exception이다.
Java의 Exception
class를 상속받은 exception class 중, RuntimeException
을 상속 하는 exception은 Unchecked Exception이며, 그렇지 않은 이외 exception은 Checked Exception이다.
For the purposes of compile-time checking of exceptions, Throwable and any subclass of Throwable that is not also a subclass of either RuntimeException or Error are regarded as checked exceptions.
위에서 잠시 보았던 것 처럼, Exception은 JVM에 의해 발생할 수 있으며, 또한 throw
를 통해 개발자가 직접 발생시킬 수도 있다. 어떠한 방식으로 발생한 예외에 대해 적절한 작업을 통해 프로그램이 복구될 수 있도록 미리 대응할 방법을 작성하는 것이 예외 처리(Exception handling)이다.
Java에서는 try - catch
를 통해 발생한 exception에 대한 대응을 수행할 수 있도록 하며 아래와 같이 작성할 수 있다.
try {
// Code that Exception occurs
} catch(Exception e) {
// recovery
}
try - catch
구문이 동작하는 방식은 아래와 같다.
try
block의 코드를 실행한다. 예외가 발생하지 않으면, try-catch
block을 벗어나 다음 코드를 실행한다.catch
block을 찾는다. (여러 catch block이 존재할 수 있다)catch
block을 찾지 못하면 예외 처리에 실패하며, 적절한 catch
block이 존재하는 경우 해당 block의 코드를 수행하고 try-catch
를 벗어나 다음 코드를 실행한다.여러 exception에 대한 처리는 아래와 같이 여러 catch
block을 작성하여 할 수 있으며,
try {
//
} catch(FileNotFoundException e) {
} catch(ArithmeticException e) {
} catch(Exception e) {
}
다른 예외에 대해 동일한 작업이 수행되는 경우 아래와 같이 작성할 수도 있다.
try {
//
} catch(FileNotFoundException | ArithmeticException e) {
} catch(Exception e) {
}
위 코드를 보면 가장 아래에 Exception
에 대한 catch
구문이 있는 것을 확인할 수 있는데, catch 구문의 예외에 또한 다형성이 적용되기 때문에, 모든 Exception에 대한 처리를 수행할 수 있으며, 적합한 catch block을 찾는 과정에서 가장 위에서 부터 찾기에 아래와 같이 작성하면, 하단의 catch 구문은 unreachable code가 된다.
try {
//
} catch(Exception e) {
} catch(FileNotFoundException | ArithmeticException e) {
// unreachable code
}
try ~ catch
구문은 발생한 예외에 대한 작업을 수행할 수 있도록 하지만, try
와 catch
또는 여러 catch
block에서 공통적으로 사용되는 코드를 따로 작성해야 하는 문제가 있다. try
block에서 예외가 발생하는 경우 예외가 발생한 코드 이후의 코드는 실행되지 않으며, 여러 catch
block이 아닌 하나의 catch
block의 코드만 실행하게 된다. 이러한 상황에서 database connection이나 file등 resource를 사용하고, 이를 반환해야 하는 코드를 작성한다면 문제가 발생할 것이다.
try {
// exception 발생하는 코드
// resource 반환 코드
} catch(Exception e) {
// exception handling 코드
// resource 반환 코드
}
위 코드에서는 하나의 catch
block만 존재하지만, 여러 catch
block이 존재한다면 resource를 반환하는 코드는 반복된다. 이러한 문제에 대한 해결책으로 try~catch~finally
를 사용할 수있다. 아래와 같이 작성할 수 있으며
try {
// exception 발생하는 코드
} catch(Exception e) {
// exception handling 코드
} finally {
// resource 반환 코드
}
finally
block에 작성한 코드는, 어떠한 상황에서도 실행된다. 심지어 return
문이 존재해도 finally
block에 작성한 코드는 실행한 후 return
이 이루어진다.
try~catch~finally
에서 자주 사용되는 패턴은 try
block에서 사용한 자원을 반환하는 경우일 것이고, 보통 close()
method를 통해 file stream이나 connection을 끊는 경우가 많다. 하지만 이러한 경우에 문제가 되는 것이, finally
block에서 해당 자원이 null
이 아닌지 확인하는 로직이 추가되는 것이다.
try {
fi = new FileInputStream("...");
} catch(IOException e) {
...
} finally {
// 1. if failed, fi will be null
if(fi != null) {
// 2. if not null, close() throws IOException
try {
fi.close();
} catch (IOException e) {
...
}
}
}
위와 같이 null check를 수행하고, close()
method를 실행할 때 발생하는 예외에 대한 처리도 추가해야 한다.(귀찮다) 이럴때 쓰라고 만든게 Java 7
부터 지원하는 try-with-resource
이다.
try(RESOURCE_TYPE1 res1 = new ..., RESOURCE_TYPE2 res2 = ...) {
// throws exception
} catch(Exception e) {
// exception handling
}
// automatically closes res1 and res2
위와 같이 try
block 옆에 사용할 resource를 작성하면, 알아서 사용한 자원을 반환해준다. 물론 모든 상황에 다 되는것은 아니다. 명시한 자원은 AutoCloseable
이라는 interface를 구현해야 하는데, 이를 구현한 FileInputStream
의 상속 구조를 보면 다음과 같다.
이를 보면 AutoCloseable
과 Closeable
interface의 상속 구조가 조금 이상한 것처럼 보일 수도 있는데, 이는 Java 7에서 try-with-resource
를 도입하는 과정에서 이전에 존재하는 Closeable
interface를 유지하기 위한 복잡한 사정이 있었던 것으로 보이는데 추후 기회가 된다면 다루어 보도록 하겠다.
프로그래밍을 처음 접할 때 C언어를 통해 시작했고, 임베디드(?)쪽 개발을 하다보니 항상 예외 상황에 대한 처리를 return -1;
등으로 처리를 해왔었다. Java를 처음 접하고 spring 개발을 하면서 처음에 가장 편하게 느낀 부분이 Exception을 통한 처리였다.
그렇다 보니 custom exception을 만들기도 하고, 이곳 저곳에서 throw
, throws
를 작성하고 catch
구문을 이곳 저곳 남발하기 시작했다. 많아지는 try-catch를 줄이기 위해 Global Exception Handler도 사용해 보았지만, 각각 상황에 따른 handling이 필요한 상황도 있었고, 오히려 불편함이 느껴지기도 했다.
이렇게 개인적으로 느끼는 불편함도 많았고, custom exception이 필요한지, 글 가장 앞에 서술한 것처럼 java의 checked exception이 필요한지 등 Java의 Exception에 대한 논쟁이 꽤나 많이 존재한다. 물론 아직 부족하고 모르는게 많기 때문이지만, 편리함을 가져다 준 것인 만큼 좀 더 생각하며 사용하고 어떻게 잘 사용할 수 있을지에 대한 고민이 필요하다고 느껴진다.