Right now do !

[JAVA] JAVA의 예외처리 - Throwable, Exception and Error

by 지금당장해

프롤로그

 필자가 프로그래밍을 하면서 가장 신경 쓰는 부분이 예외처리다. C# 관련 글에도 예외 처리를 쓴 적이 있는데 완수를 못해서 늘 마음에 걸린다. 지금은 어찌하다 보니 현 개발 프로젝트가 JAVA가 되는 바람에 상당기간 동안 JAVA를 주력 언어로 써야 한다. 그래서 C#은 나중으로 미루고 JAVA의 예외 처리를 먼저 정리해보려 한다. 이번 글에서는 JAVA의 예외 처리 Class들에 대해서 고민해보는 시간을 먼저 갖으려 한다.

 

Throwable, Exception, Error

Throwable Extends(출처: https://howtodoinjava.com/best-practices/java-exception-handling-best-practices/)

 위 그림은 JAVA의 예외처리 Class들의 상속체계이다. 가장 super 위치에 Throwable 이 자리 잡고 (있다. 그렇다 모든 예외 처리 Class의 super base이다. 모든 예외나 에러가 이 Class로부터 파생이 된다. (C#에서는 이 위치에 Exception이 있다.) 그리고 이를 상속받은 Exception과 Error가 존재한다. Error 시스템적인 예외를 의미한다. 해서 Effective JAVA와 같은 책에서는 이 클래스를 개발자가 상속받지도 잡지도(catch사용) 말라한다. 이 예외는 그만큼 심각한 상황에서 발생한다. (일부는 컴파일 시에 발생하기도 한다.) 이런 오류가 발생하면 응용프로그램에서 할 수 있는 일은 전역 catch 하여 로그 찍고 프로그램 종료 시는 것이 최선일 것이다.(이 방법에 대해서는 다음 편에서 다루겠다.)

Checked, Uncheckd Exception

 C# 개발자에게는 참으로 생소한 개념이다. 함수 옆구리에 throws라고 쓰고 거기에 이 함수에서 발생할 수 있는 예외를 나열한다. 그것도 전부가 아니라 NPE(널 포인트 예외) 같은 소위 런타임 예외는 예외란다. 첨에는 짜증이 ~ "그렇게 언어 스펙을 만들었다는데 어쩔겨 이해하고 써야지" 하고 이제 적응을 했다.

public static void throwsException(int millisecond) throws IOException {
        
    File file = new File("/test.txt");
    // createNewFile()에서 IOException이 발생하므로 함수 선언부에 throws를 명시하였다. 
    boolean b = file.createNewFile();    

 먼저 이렇게 throws에 명시를 해야 하는 것과 그렇지 않은 것에 대한 차이를 알아보자. 명시를 해야 하는 것은 검사 예외 즉 Checked Exception이다. 목적은 함수 수행 중에 날만한 예외이니 호출하는 측에서는 이를 대비하라. 즉 예외가 있는지 검사하고 예외 발생 시 조치하라 이다. 필자가 쓴 thread 관련된 글에서 만이 등장했던 InterruptedException이 그 대표적인 예이다. 아래 예제를 해석해보면 이렇다 sleep() 함수는 interrupt() 함수 호출 시 InterruptedException이 발생할 수 있으니 대비해야 한다는 것이다. 

 

public static void sleep(int millisecond) {
    try {
        Thread.sleep(millisecond);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

이 코드와 같이 호출부에서 try ... catch로 처리하지 않으면 해당 함수에서 throws 해야 한다. (throwsException 함수 예제처럼) 그렇다 이 의미는 호출부에서 처리하지 않고 이 함수를 호출하는 호출자에게 책임을 떠 넘기겠다는 의미다. 그러나 특별한 이유가 없다면 예외를 검출할 수 있는 최 전방의 위치에서 잡아서 Logging을 하던 로직에 이를 반영하던 하는 것이 좋다. 이 검사 예외는 통상 발생하더라도 응용프로그램을 계속 운영할 수 있는 상황에서 사용한다. 

 이제 반대로 비 검사 예외(Uncheced Exception)에 대해 알아보자. 이 예외는 Exception을 상속받았다. 헷갈리지 말자 Exception을 상속받으면 검사예외가 되는데 이를 상속받은 RuntimeException은 비 검사 예외이다. 필자가 JAVA를 설계했다면 Throwable을 abstract로 하고 이를 이를 상속받아 Exception과 RuntimeException을 만들었을 것이다. 사실 이름도 마음에 안 든다. 다시 본론으로 돌아가 비 검사 예외는 RuntimeException과 Error을 확장한 것들이다. (헷갈리면 처음 그림을 다시 보라.) 이는 검사 예외와 반대로 호출자가 예외를 처리할 의무가 없다. 예외를 아무도 안 잡으면 이를 실행시킨 JVM이 받아 예외처리를 해준다. 이 두 예외는 발생하더라도 응용프로그램에서 대응할 조치가 없는 경우가 많다. RuntimeException이 발생한 경우 때로는 예외를 처리해야 하는 경우도 있다.  그러나 과도한 예외 검출은 금물이다. 시스템 성능을 떨어뜨린다. 그래서인지 여러 여러 자료에서 이런 성격의 예외를 여기저기서 잡지 않을 것을 권장한다. 통상 응용프로그램 레벨에서 Unchecked를 검출하여 로깅을 하고 프로그램을 종료시키는 방법이 사용된다.

// 이 코드는 무조건 예외가 발생한다. 
// 그러나 Unchecked예외인 NullPointerException에 대해서는 어떤 의무도 없다.
public void alwaysThrowsNpe() {
    Object obj = null;
    obj.toString();
}

에필로그

 본 글의 핵심 두 가지는 JAVA의 예외처리 Class의 상속 계층 이해와 Chected, Uncheck 예외의 차이점 이해다. 더 낳아가 이 두 가지를 어떻게 구별해서 쓸 것인가에 대한 고찰이다. 다음 글에서는 이 이해를 바탕으로 응용프로그램 개발자 입장에서 예외 처리를 어떻게 해야 하는지 좀 더 심도 깊은 고민을 해보려 한다.

 

블로그의 정보

지금 당장 해!!!

지금당장해

활동하기