그림에서 볼 수 있듯이 Error와 Exception은 다르다.
Error는 시스템 내 비정상적인 상황이 생겼을 때 발생한다. 따라서 시스템 레벨에서 발생하며, 개발자가 미리 처리할 수도 예측할 수도 없다.
Exception은 개발자가 구현한 로직 내에서 발생한다. 따라서 Exception은 미리 예측하여 처리할 수 있다.
Exception 클래스를 상속한 하위 클래스는 RuntimeException을 기준으로 구분된다. Exception의 자식 중 RuntimeException을 제외한 모든 클래스는 Checked Exception이며, RuntimeException과 그의 하위 클래스들은 Unchecked Exception이라 부른다.
예외를 던져야 할 때 해당 예외에 대한 명확한 복구 방법이 정의돼 있다면 Checked Exception, 그렇지 않은 경우에는 Unchecked Exception 이라고 한다. 즉, Compile 단계에서 Unchecked Exception은 체크되지 않는다는 것이다.
Checked Exception은 컴파일 단계에서 확인할 수 있으며, 반드시 예외 처리를 해야 한다. 예외발생 시 Roll-back 하지 않는다. (e.g. IOException/SQLException/FileNotFoundException...)
Unchecked Exception은 반드시 예외 처리를 할 필요는 없다. Runtime에서 확인할 수 있고 예외 처리 시 Roll-back을 해야 한다.
표로 나타내면 아래와 같다.
CheckedException | UncheckedException | |
---|---|---|
처리여부 | 반드시 예외 처리를 해야 한다. | 명시적인 처리를 강제하지 않는다. |
확인시점 | 컴파일 단계 | 실행 단계 |
예외발생시 트랜잭션 처리 | Roll-back X | Roll-back O |
대표 예외 | IOException, QLException, FileNotFoundException... | RuntimeException 하위 클래스(NullPointerException, IllegalArgumentException, IndexOutOfBoundException, SystemException) |
예외 복구를 이용해 예외가 발생하여도 애플리케이션이 계속해서 정상적인 흐름을 진행한다.
int maxRetryCnt = MAX_RETRY;
while (maxRetryCnt-- > 0) {
try {
return;
}
catch (NewException e) {
...
}
finally {
...
}
}
throw new RetryFailedException();
위의 코드는 일정 횟수만큼 재시도를 반복하여 예외를 복구하는 코드이다. 불안정적인 네트워크 환경에서의 서버 접속을 원활하게 하는 시스템 내에서 적용할 때 효율적이겠다.
또는 메소드를 나누어 진행하는 방법도 있다.
public static void main(String[] args) {
try {
String test = method("1");
if (test == null) { ... }
}
catch (Exception e) {
}
}
private static String method(String str) throws Exception {
if (str.equals("1")) return null;
else if (str.equals("2")) throw new Exception("Throw exception");
else return str;
}
위와 같은 코드에서 예외를 복구하는 방식으로 작성한다면 아래와 같다.
private static String method(String str) {
try {
if (str.equals("1")) return null;
else if (str.equals("2")) throw new Exception("Throw exception");
} catch(Exception e) {
return null;
}
}
public void add() throws SQLException {
...
}
위 코드는 throws를 통해 호출한 쪽으로 예외를 던지고 그 처리를 회피하는 것이다.
무책임하게 던지기만 해서는 안 되는 신중해야 할 로직이다. 호출한 쪽에서 다시 예외를 받아 처리하도록 하거나, 해당 메소드에서 이 예외를 던지는 것이 최선의 방법이라는 확신이 있을 때만 사용해야 한다.
catch(SQLException e) {
...
throw DuplicateUserIdException();
}
예외를 잡아 다른 예외를 던지는 것이다. 호출한 쪽에서 예외를 받아 처리할 때 좀 더 명확하게 인지할 수 있도록 돕기 위한 방법이다.
예를 들어, Checked Exception 중 복구가 불가능한 예외가 잡혔다면 이를 Unchecked Exception 으로 전환해서 다른 계층에서 일일이 예외를 선언할 필요가 없도록 할 수도 있다.
예외 처리는 실패하지 않는 방법이다. 언어를 공부하고 코드를 짜서 동작하게 만드는 것을 성공하는 법이었다고 하면, 그 과정에서 발생할 수 있는 실패를 방지하거나 덜 실패하게끔 만드는 것이 예외이다.
예외를 잡았을 때 아무런 처리도 하지 않는 것은 정말 위험하다. try/catch문에서 catch를 비워두면 컴파일 오류는 벗어날 수 있지만, 예외 발생 시 그 원인을 파악하기 어려워 유지보수만이 아닌 개발까지도 치명적이다. 따라서 어떤 처리를 해야 하는지 모르더라도 무작정 catch를 비워두거나 throw해버리는 행위는 신중해야 한다.
시스템은 고객의 요구사항을 만족시키는 것도 중요하지만, 작동되어서는 안 될 기능이 작동되는 것을 막는 것도 매우 중요하다. 신뢰 있는 개발자가 되기 위해서는 예외처리가 필수적이다.
참고 자료
Java 예외(Exception) 처리에 대한 작은 생각
생활코딩-자바(JAVA)
Recover from a method throwing an exception
Checked Exception & Unchecked Exception