자바의 예외의 종류 3가지

de_sj_awa·2021년 4월 25일
3

1. 자바의 예외의 종류 3가지

자바에서 예외란 "우리가 예상한, 혹은 예상치도 못한 일이 발생하는 것을 미리 예견하고 안전장치를 하는 것"을 말한다.

자바에서는 세 종류의 예외가 존재하며 각 예외는 다음과 같다.

  • checked exception
  • error
  • runtime exception 혹은 unchecked exception

각 예외의 구분은 간단하다. 두 번째와 세 번째에 있는 error와 unchecked exception을 제외한 모든 예외는 checked exception이다.

1. error (이하 에러)

에러는 자바 프로그램 밖에서 발생한 예외를 말한다. 가장 흔한 예가 서버의 디스크가 고장 났다든지, 메인보드가 맛이 가서 자바 프로그램이 제대로 동작하지 못하는 경우가 여기에 속한다. 다음 절에서 배우겠지만, Exception 클래스는 에러가 아니다. 뭔가 자바 프로그램에 오류가 발생했을 때, 오류의 이름이 Error로 끝나면 에러이고 Exception으로 끝나면 예외이다.

Error와 Exception으로 끝나는 오류의 가장 큰 차이는 프로그램 안에서 발생했는지, 밖에서 발생했는지 여부이다. 하지만, 더 큰 차이는 프로그램이 멈추어 버리느냐 계속 실행할 수 있느냐의 차이이다. 더 정확하게 말하면 Error는 프로세스에 영향을 주고, Exception은 쓰레드에만 영향을 준다.

2. runtime exception (이하 런타임 예외)

런타임 예외는 예외가 발생할 것을 미리 감지하지 못했을 때 발생한다. 이 런타임 예외에 해당하는 모든 예외들은 RuntimeException을 확장한 예외들이다. 이 예외를 묶어주지 않는다고 해서 컴파일 할 때 예외가 발생하지 않는다. 하지만, 실행시에는 발생할 가능성이 있다. 그래서, 이러한 예외들을 런타임 예외라고 한다. 그리고, 컴파일시에 체크를 하지 않기 때문에 unchecked exception이라고도 부르는 것이다.

지금까지 설명한 내용을 바탕으로 예외들의 상관 관계를 그림으로 나타내면 다음과 같다.

여기서 Exception을 바로 확장한 클래스들이 Checked 예외이며, RuntimeException 밑에 확장되어 있는 클래스들이 런타임 예외들이다.

2. Throwable 클래스

Exception과 Error의 공통 부모 클래스는 당연히 Object 클래스이다. 그리고, 공통 부모 클래스가 또 하나 있는데, java.lang 패키지에 선언된 Throwable 클래스다. 다시 말해서 Exception과 Error 클래스는 Throwable 클래스를 상속받아 처리하도록 되어 있다.
그래서, Exception이나 Error를 처리할 때 Throwable로 처리해도 무관하다.
상속 관계가 이렇게 되어 있는 이유는 Exception이나 Error의 성격은 다르지만, 모두 동일한 이름의 메소드를 사용하여 처리할 수 있도록 하기 위함이다. 그러면 가장 먼저 Throwable에 어떤 생성자가 선언되어 있는지 살펴보자.

  • Throwable()
  • Throwable(String message)
  • Throwable(String message, Throwable cause)
  • Throwable(Throwable cause)

아무런 매개 변수가 없는 생성자는 기본적으로 생성된다. 그리고, 예외 메시지를 String으로 넘겨줄 수도 있다. 그리고, 별도로 예외의 원인을 Throwable 객체로 넘겨 줄 수도 있다.

Throwable 클래스에 선언되어 있고, Exception 클래스에서 Overriding한 메소드는 10개가 넘는다. 그 중 가장 많이 사용되는 메소드는 다음과 같다.

1. getMessage()

예외 메시지를 String 형태로 제공받는다. 예외가 출력되었을 때 어떤 예외가 발생되었는지를 확인할 때 매우 유용하다. 즉, 그 메시지를 활용하여 별도의 예외 메시지를 사용자에게 보여주려고 할 때 좋다.

2. toString()

예외 메시지를 String 형태로 제공받는다. 그런데, getMessage() 메소드보다는 약간 더 자세하게, 예외 클래스 이름도 같이 제공한다.

3. printStackTrace()

가장 첫 줄에는 예외 메시지를 출력하고, 두 번째 줄부터는 예외가 발생하게 된 메소드들의 호출 관계(스택 트레이스)를 출력해준다.

예제 코드

public class ThrowableSample {

    public static void main(String[] args){
        ThrowableSample sample = new ThrowableSample();
        sample.throwable();
    }
    public void throwable(){
        int[] intArray=new int[5];
        try{
            intArray=null;
            System.out.println(intArray[5]);
        }catch(Throwable t){
            System.out.println(t.getMessage());
            System.out.println(t.toString());
            t.printStackTrace();
        }
    }
}

실행 결과

3. throws와 throw

자바에서는 예외를 발생시킬 수 있다.

예제 코드

package exception;

public class ThrowSample {

    public static void main(String[] args){
        ThrowSample sample = new ThrowSample();
        sample.throwException(13);
    }
    public void throwException(int number){
        try{
            if(number>12){
                throw new Exception("Number is over than 12");
            }
            System.out.println("Number is " + number);
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

1년 열두 달을 처리하는 로직이 필요하다고 하자. 메소드의 매개 변수로 13월이 넘어왔다면, 정상적인 처리가 불가능할 것이다. 이러한 상황에서 try 블록 내에서 throw라고 명시한 후 개발자가 예외 클래스의 객체를 생성하면 된다. 그러면 다른 예외가 발생한 상황과 동일하게 throw한 문장 이후에 있는 모든 try 블록 내의 문장들은 수행이 되지 않고, catch 블록으로 이동한다.

catch 블록 중에 throw한 예외와 동일하거나 상속 관계에 있는 예외가 있다면 그 블록에서 예외를 처리할 수 있다. 여기서는 e.printStackTrace() 메소드를 호출하기 때문에 예외 스택 정보가 출력될 것이다.

만약 해당하는 예외가 없다면 발생된 예외는 메소드 밖으로 던져버린다. 즉, 다시 말해서 예외가 발생한 메소드를 호출한 메소드로 던진다는 의미이다. 이럴 때 사용하는 것이 throws 구문이다. throws 구문은 다음과 같이 메소드 선언할 때 사용하면 된다.

 public void throwsException(int number) throws Exception{
        if(number>12){
            throw new Exception("Number is over than 12");
        }
        system.out.println("Number is " + number);
    }

이렇게 메소드 선언에 해놓으면, 예외가 발생했을 때 try~catch로 묶어주지 않아도 그 메소드를 호출한 메소드로 예외 처리를 위임하는 것이기 때문에 전혀 문제가 되지 않는다.
이렇게 try~catch 블록으로 묶지 않아도 예외를 throw한다고 할지라도, throws가 선언되어 있기 때문에 전혀 문제없이 컴파일 및 실행이 가능하다. 하지만, 이렇게 throws로 메소드를 선언하면 개발이 어려워진다. 왜냐하면, 이 throwsException()이라는 메소드는 Exception을 던진다고 메소드 선언부에 throws 선언을 해놓았기 때문에, throwsException() 메소드를 호출한 메소드에서는 반드시 try~catch 블록으로 throwsException() 메소드를 감싸주어야 한다.

따라서 try~catch 블록으로 묶지 않으면 컴파일 에러가 발생할 것이다. 그러므로 이러한 throws 문장으로 인한 컴파일 오류가 생겼을 경우에는 두 가지 방법이 있다.

  1. 하나는 다음과 같이 try~catch로 묶는 것이다.
    public static void main(String[] args){
        ThrowSample sample = new ThrowSample();
        sample.throwException(13);
        try {
            sample.throwsException(13);
        }catch(Exception e){
            
        }
    }
  1. 다른 한 가지 방법은 호출한 메소드(여기서는 main() 메소드)에서도 다시 throws 해버리면 된다.
    public static void main(String[] args) throws Exception{
        ThrowSample sample = new ThrowSample();
        sample.throwException(13);
        sample.throwsException(13);
    }

이렇게 해놓다도 컴파일하고 실행하는 데 전혀 문제가 없다. 하지만, 이미 throws 한 것을 다시 throws하는 방법은 그리 좋은 습관이 아니다. 가장 좋은 방법은 throws 하는 메소드를 호출하는 메소드에서 try~catch로 처리하는 것이다. 이 클래스를 컴파일하고 실행하면 throwsException() 메소드를 호출할 경우 다음과 같이 예외 메시지가 나타난다.

지금까지 배운 예외의 throws와 throw에 대해서 정리해보자.

  • 메소드를 선언할 때 매개 변수 소괄호 뒤에 throws라는 예약어를 적어준 뒤 예외를 선언하면, 해당 메소드에서 선언한 예외가 발생했을 때 호출한 메소드로 예외가 전달된다. 만약 메소드에서 두 가지 이상의 예외를 던질 수 있다면, implements처럼 콤마로 구분하여 예외 클래스 이름을 적어주면 된다.
  • try 블록 내에서 예외를 발생시킬 경우에는 throw라는 예약어를 적어준 뒤 예외 객체를 생성하거나, 생성되어있는 객체를 명시해준다. throw한 예외 클래스가 catch 블록에 선언되어 있지 않거나 throws 선언에 포함되어 있지 않으면 컴파일 에러가 발생한다.
  • catch 블록에서 예외를 throw할 경우에도 메소드 선언의 throws 구문에 해당 예외가 정의되어 있다.

예외를 throw하는 이유는 해당 메소드에서 예외를 처리하지 못하는 상황이거나, 미처 처리하지 못한 예외가 있을 경우에 대비하기 위함이다.

4. 자바 예외 처리 전략

여기서 Exception을 바로 확장한 오른쪽에 있는 예외들이 Checked 예외이고, 왼쪽 하단에 RuntimeException을 확장한 예외들이 런타임 예외(혹은 Unchecked 예외)이다.

예외를 직접 만들 때 Exception 클래스를 확장하여 나만의 예외 클래스를 만들었다. 그런데, 이 예외가 항상 발생하지 않고, 실행시에 발생할 확률이 매우 높은 경우에는 런타임 예외로 만드는 것이 나을 수도 있다. 즉, 클래스 선언시 extends Exception 대신에 extends RuntimeException으로 선언하는 것이 낫다. 이렇게 되면, 해당 예외를 던지는(throw하는) 메소드를 사용하더라도 try~catch로 묶지 않아도 컴파일시에 예외가 발생하지 않는다. 하지만, 이 경우에는 예외가 발생할 경우 해당 클래스를 호출하는 다른 클래스에서 예외를 처리하도록 구조적인 안전 장치가 되어 잇어야만 한다. 여기서 안전 장치라고 하는 것은 try~catch로 묶지 않은 메소드를 호출하는 메소드에서 예외를 처리하는 try~catch가 되어 있는 것을 의미한다.

unchecked exception인 RuntimeException이 발생하는 메소드가 있다면, 그 메소드를 호출하는 메소드는 try~catch로 묶어주지 않아도 컴파일할 때 문제가 발생하지는 않는다. 하지만, 예외가 발생할 확률은 높으므로 try~catch로 묶어주는 것이 좋다.

또한 개발 표준을 잡을 때 catch문 내에서 어떻게 처리할지를 명시적으로 선언해 두어야만 한다. 다시 말해서 catch문에서 로그를 남기는 등의 작업을 하고 예외를 throw를 사용해서 던져 주어야 문제가 발생한 정확한 원인을 알 수 있다.

정리해 보자면,

  • 임의의 예외 클래스를 만들 때에는, 반드시 try~catch로 묶어줄 필요가 있을 때에만 Exception 클래스를 확장한다. 일반적으로 실행시 예외를 처리할 수 있을 경우에는 RuntimeException 클래스를 확장하는 것을 권장한다.
  • catch문 내에 아무런 작업 없이 공백을 놔두면 예외 분석이 어려워지므로 꼭 로그 처리와 같은 예외 처리를 해줘야만 한다.

참고

  • 자바의 신
profile
이것저것 관심많은 개발자.

0개의 댓글