[Live Study] #9 예외 처리

Jihoon Oh·2021년 2월 5일
0

Live Study

목록 보기
9/13
post-thumbnail

목표

자바의 예외 처리에 대해 학습하세요.

학습할 것 (필수)

  • 자바에서 예외 처리 방법 (try, catch, throw, throws, finally)
  • 자바가 제공하는 예외 계층 구조
  • Exception과 Error의 차이는?
  • RuntimeException과 RE가 아닌 것의 차이는?
  • 커스텀한 예외 만드는 방법

학교에서 객체지향 프로그래밍 수업을 통해 자바를 처음 배웠을 때, 가장 아무 생각없이 썼던 구문이 try ~ catch 예외 처리 구문이었던 것 같다. 어떤 메소드가 왜 try catch나 throws를 사용해서 예외처리를 해 주는지에 대한 이해 없이, 그저 컴파일이 안되니까 try catch를 넣고 catch안에 printstack으로 에러를 죽 출력해주는 정도에 그쳤던 것 같다. 이번 기회에 예외 처리에 대해서 세세히 짚고 넘어가는 좋은 기회가 되지 않을까 기대한다.

예외(Exception)란? Exception과 Error의 차이는?

필수 학습할 것 에는 없지만, 예외 처리에 앞서 예외의 개념부터 정확히 짚고 넘어가야 할 것 같다. 흔히들(나도 그랬고) 예외(Exception)와 오류(Error)를 명확히 구분하지 못하고 헷갈리곤 한다. 오류와 예외 모두 일반적인 프로그램의 실행 흐름에 영향을 주어 프로그램이 멈추거나 한다. 하지만 둘에는 명확한 차이가 존재한다.

먼저 오류는 시스템 레벨에서 비정상적인 상황이 발생했을때 생긴다. 예를 들면, 하드웨어가 고장이 나거나 JVM에 오류가 나는 등 프로그램을 정상 구동할 수 없는 치명적인 상황이 오류라고 할 수 있다. 이런 오류는 개발자가 미리 예측하여 처리할 수 없는 수준이기 때문에, 오류에 대한 처리는 생각하지 않아도 된다.

반면 예외는 개발자가 예측할 수 있는 상황이다. 시스템 레벨에서 발생하는 오류와 달리 예외는 개발자가 짠 로직을 수행하는 과정에서 발생한다. 오류가 발생하면 시스템이 꼼짝없이 멈추게 되지만, 예외의 경우에는 개발자가 발생할 수 있는 예외를 예측하고 해당 예외에 대한 처리 로직을 짜 주면 프로그램이 정상적으로 작동을 이어나갈 수 있게 해준다.

참고로, 다른 언어 중에는 예외 처리가 필수적이지 않은(견고한 프로그램을 만들기 위해서는 필요하나, 예외 처리를 하지 않는다고 컴파일에 제약이 걸리지 않는) 언어들이 있는 반면, 자바는 후술할 Exception 클래스를 상속하는 예외들이 발생할 수 있다면, 예외 처리를 하지 않을 경우 컴파일러가 아예 컴파일을 거부해 버린다.

자바에서 예외 처리 방법

자바의 가장 기본적인 예외 처리 방법은 try ~ catch 구문이다. try와 catch 뒤에는 중괄호로 로직을 감싼다. 예외가 발생한다고 예상하는 로직을 try에, 처리할 예외와 방법을 catch에 작성하여 예외를 처리한다. try문 내의 로직을 처리하다가 예외가 발생한다면, try 구문을 종료하고 발생한 예외를 처리하는 catch 구문을 실행한다.

try {
    // 예외가 발생할 수 있는 로직
    ...
} catch (에러 1) {
    // 에러 1이 발생했을 때 실행할 로직
    ...
} catch (에러 2) {
    // 에러 2가 발생했을 때 실행할 로직
    ...
}

이처럼 하나의 try 구문에서 여러 개의 에러가 발생할 수 있다면 catch를 여러개 붙여 쓰는 것도 가능하다. 이때 에러 1, 에러 2 부분, 즉 catch 뒤에 오는 괄호 안에는 예외 클래스를 넣어서 예외를 구분해준다. 일반적으로 많이 쓰이는 예시는 다음과 같다.

Scanner scanner = new Scanner(System.in);
try {
    int number = scanner.nextInt();
} catch (InputMismatchException e) {
    e.printStackTrace();
} catch (Exception e) {
    // 예외 처리 로직
}

여기서 number 변수에 int값이 아닌 다른 값이 들어갈 때 발생하는 InputMismatchException을 catch(InputMismatchException e) 구문에서 처리해준다. 그 뒤에 나온 catch (Exception e)는 모든 예외에 대한 예외 처리가 가능한 구문인데, 세세한 예외 구별을 하지 않아도 되기는 하지만, 프로그램의 견고함을 떨어뜨리기 때문에 비추천하는 방법이다.

try ~ catch 뒤에는 추가로 finally 구문이 올 수 있다. finally는 말 그대로 최종적으로 실행하는 로직이 들어가는데, try 문에서 예외가 발생하여 catch 구문이 실행되는것과 관계없이 무조건 실행하는 구문이다. 예외의 발생 여부와 상관 없이 반드시 처리해야 하는 작업이 있을 때 사용한다. 예를 들어, 데이터베이스에 접속해서 데이터 처리를 하는 메소드를 작성할 때 반드시 예외 처리를 해 주어야 하는데, 예외 발생과는 상관 없이 프로그램 종료 전에 데이터베이스와의 연결을 해제해 주어야 하므로 데이터베이스 접속을 해제하는 로직이 반드시 실행되도록 코드를 짜서 넣어주어야 한다. 이럴 때 finally를 사용할 수 있다.

일반적으로는 try ~ catch (~ finally)를 많이 쓰기는 하지만, try ~ catch를 쓰지 않고 다른 방법으로도 예외 처리가 가능하다. 바로 throws 키워드를 이용하는 방법이다. 예외가 발생할 수 있는 메소드에 throws 키워드를 사용하면 예외 처리를 해줄 수 있다.

public void readBuffer(byte[] buffer) throws IOException {
    System.in.read(buffer);
}

이렇게 메소드에서 예외 처리 로직을 넣지 않을 수 있다. 그러나 throws를 통한 예외 처리는 여기가 끝이 아니다. throws는 예외를 현재 메소드에서 처리하지 않고 메소드를 호출한 다른 메소드로 예외 처리를 떠넘긴다. 즉, 만약 main 메소드에서 throws로 예외 처리를 한 readBuffer 메소드를 호출한다면, main의 readBuffer를 호출하는 부분에서 try ~ catch를 사용해서 다시 예외 처리 코드를 짜줘야 한다.

만약 throws로 던지는 예외가 메인 메소드까지 가서도 처리가 되지 않는다면, 해당 예외가 발생했을 때 프로그램은 적절한 예외 처리를 하지 못하고 종료된다.

개발자가 예외를 직접 발생시킬 수도 있다. 자바에서 규정하는 예외 상황 외에도 개발자가 직접 예외 오브젝트를 만들어서 throw 키워드로 던질 수 있다. throw 뒤에는 java.lang.Throwable 클래스의 자식 클래스 객체를 지정해야 한다.

public void doSomething () throws Exception {
    ...
    if (...) {
        throw new Exception();
    }
    ...
}

이런 식으로 개발자가 원하는 상황에 새로운 예외 객체를 만들어서 throw 해줄 수 있다. throw도 throws와 마찬가지로 예외 처리를 떠넘긴다. 하지만 throws가 해당 메소드의 상위 메소드로 예외 처리를 넘긴다면, throw는 상위 블록으로 넘긴다는 차이가 있다.

때문에 위의 예제 코드를 보면, if 블록에서 Exception 객체를 만들어서 던지면, doSomething 메소드의 최상위 블록에서 다시 한번 처리를 해 주어야 하기 때문에 throws로 상위 메소드로 해당 Exception을 다시 던지는 것을 볼 수 있다.

throw는 새로운 예외를 만들어서 처리하는데도 쓰이지만, 일반적인 예외를 try ~ catch 등으로 처리한 후 다른 종류의 예외를 다시 던지거나 throws와 연계하여 상위 메소드로 예외의 내용을 전달하고 싶을 때 사용할 수도 있다.

자바가 제공하는 예외 계층 구조

자바의 예외 계층 구조는 위 그림과 같다. 기본적으로 에러와 예외 모두 Throwable을 상속하고, Exceptions 객체는 다시 세분회된 예외 클래스들로 상속된다.

빨간색으로 표시된 Checked Exception은 예외 처리가 되어있지 않다면 컴파일러가 컴파일 자체를 거부하는 예외들이고, 파란색 Unchecked Exception은 예외 처리를 하지 않더라도 컴파일러가 컴파일 하는 예외다. (단, Unchecked Exception이 예외 처리 없이 발생하면 프로그램이 멈춘다.)

RuntimeException과 RE가 아닌 것의 차이는?

RuntimeException은 자바에서 발생하는 예외들 중에 유일하게 Unchecked 예외의 범주에 들어가는 예외다. 앞서 Unchecked Exception은 예외 처리를 하지 않아도 컴파일이 된다고 했는데, RuntimeException들은 주로 개발자의 실수에 의해 발생하는 예외들이 많기 때문에, 개발자가 코드에 조금 더 신경 쓰면 피할 수 있는 예외들이기 때문에 컴파일러가 체크하지 않는다. 반면 그 외의 Exception들은 외부의 영향으로 인해 발생할 수 있는 예외들이기 때문에 반드시 예외 처리를 하도록 컴파일러가 예외 미처리시 컴파일을 거부한다.

RuntimeException에는 대표적으로 ArithmeticException(산술 예외, 0으로 나누기), IndexOutOfBoundsException(배열범위를 벗어난 예외) 등이 있는데, 이런 예외들 역시 try ~ catch 등으로 처리가 가능하지만, try ~ catch는 자원을 많이 소모하기 때문에 개발자들이 코드를 주의 깊게 작성하여 해당 예외가 발생하지 않도록 해야 한다.

커스텀한 예외 만드는 방법

일반적인 예외들은 모두 자바에서 만들어서 java.lang 패키지 안에 들어있다. 그러나 이 예외들이 아닌 다른 예외 상황을 처리하고 싶을 때, 개발자는 자체적으로 커스텀한 예외를 만들어서 사용할 수 있다.

public class CustomException extends Exception {
    public CustomException(String msg) {
        super(msg);
    }
}

위 예제 코드처럼 예외 클래스를 상속받는 것으로 예외 객체를 만들 수 있다. 이렇게 만든 예외 객체는 코드 내에서 앞서 설명했던 throw(의도적으로 예외 발생)를 통해 발생시켜서 처리할 수 있다.

이 때 만약 CustomException이 RuntimeException을 상속한다면, CustomException이 발생할 때 예외 처리를 해주지 않더라도 컴파일러가 정상적으로 컴파일한다.

profile
Backend Developeer

0개의 댓글