본문 바로가기
자바/스터디

[자바 스터디] 9주차 과제 - 예외 처리

by jeonghaemin 2021. 1. 15.
728x90

본 게시글은 백기선 님의 live-study 과제를 수행하면서 작성한 글입니다.

목표

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

학습할 것 (필수)

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

Exception과 Error

실행 중인 프로그램이 오작동을 하거나 비정상적으로 종료되도록 하는 원인이 되는 것을 에러라고 한다.

  • 컴파일 에러 : 컴파일 시에 발생하는 에러
  • 런타임 에러 : 실행 시에 발생하는 에러
  • 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

런타임 에러를 방지하기 위해서는 프로그램 실행 도중 발생할 수 있는 모든 경우의 수를 고려하여 대비를 해야 한다. 자바에서는 런타임 시에 발생할 수 있는 프로그램 에러를 'Error'와 'Exception' 2가지로 구분한다.

  • 에러 : 프로그램 코드에 의해서 수습될 수 없는 메모리 부족이나 스택오버플로우와 같이 발생하면 복구할 수 없는 심각한 에러.
  • 예외 : 프로그램 코드에 의해서 수습될 수 있는 비교적 덜 심각한 에러. 개발자가 적절한 코드를 작성하여 비정상적인 종료를 막을 수 있다.

예외 클래스의 계층구조

https://madplay.github.io/post/java-checked-unchecked-exceptions

자바에서는 Exception과 Error를 클래스로 정의하였다.

모든 예외의 최고 조상은 Excepton 클래스이며, 위 그림에서 볼 수 있듯이 두 가지 분류로 나눌 수 있다.

  • RuntimeException과 자손들을 제외한 Exception 클래스와 그 자손들
  • RuntimeExceptino과 그 자손들

RuntimeException 클래스들은 주로 프로그래머의 실수로 인해 발생될 수 있는 예외들이다. 몇 가지 예를 들어 보면 다음과 같다.

  • ArrayIndexOutOfBoundsException : 배열의 범위를 벗어난 인덱스에 대한 값을 요청했을 때
  • NullPointerException : null인 참조 변수의 멤버를 호출했을 때
  • ClassCastException : 클래스 간의 형 변환을 잘못했을 때
  • ArithmeticException : 정수를 0으로 나눌 때

Exception 클래스들은 주로 외부의 영향으로 발생할 수 있는 것들로, 프로그램 사용자들의 동작에 의해 발생하는 경우가 많다. 몇 가지 예를 들어보면 다음과 같다.

  • FileNotFoundException : 존재하지 않는 파일의 이름을 불러오려고 할 때
  • ClassNotFoundException : 클래스의 이름을 잘못 적었을 때
  • DataFormatException : 입력 데이터의 형식이 잘못됐을 때

RuntimeException클래스는 개발자의 실수에 의해 발생될 수 있는 예외이기 때문에 예외처리를 강제하지 않아 'Unchecked Exception', Exception클래스는 예외처리를 필수로 해야 해서 'Checked Exception'라고 부르기도 하는데 Exception클래스의 경우 예외를 처리해주지 않으면 컴파일 시에 에러가 발생하는 반면 RuntimeException은 예외처리를 해주지 않아도 성공적으로 컴파일이 된다.

예외 처리하기

try-catch

자바에서는 기본적으로 try-catch문을 사용하여 예외를 처리하며, 기본적인 구조는 다음과 같다.

try {
        //예외가 발생할 가능성이 있는 코드들을 넣는다.
} catch(Exception1 e1) {
    //try블럭에서 Exception1이 발생했을 경우, 이를 처리하기 위한 코드를 넣는다.
} catch(Exception2 e2) {
    //try블럭에서 Exception2이 발생했을 경우, 이를 처리하기 위한 코드를 넣는다.
} catch(Exception3 e3) {    
    //try블럭에서 Exception3이 발생했을 경우, 이를 처리하기 위한 코드를 넣는다.
        try { }    catch(Exception4 e4) {} // 중첩하여 사용 가능하다.
} finally {
        //예외의 발생 여부와 상관없이 항상 수행되어야하는 코드를 넣는다.
}

try블록안에서 예외가 발생하면 try블럭의 나머지 코드를 더이상 실행하지 않고 발생한 예외와 일치한 catch블럭이 있는지 위에서부터 순차적으로 확인해나가다가 일치하는 예외를 발견하면 해당 catch 블럭 안의 코드를 실행하고 try-catch 문을 빠져나간다. catch 블록에서 적 적절한 예외를 발견하지 못하면 해당 예외는 처리되지 못하게 된다.

또한 catch문 안에 여러 개의 try-catch문을 중첩해서 사용할 수도 있다.

마지막에 있는 finally 블록은 try 블록에서 예외가 발생하건, 안하건 무조건 실행되어야 하는 코드가 있을 때 선택 적으로 사용한다. fianlly블록은 항상 try-catch문의 마지막에 위치해야 한다.

멀티 catch블록

JDK1.7부터는 '|'를 사용하여 여러 catch 블록을 하나의 catch 블록으로 합칠 수 있는 '멀티 catch블록'이 추가되었다.

Throw

throw키워드를 사용해서 개발자가 의도적으로 예외를 발생시킬 수 있다.

try {
        throw new Exception("의도적으로 Exception예외 발생시킴!");
} catch(Exception e) {}

Throws

메소드에 throws 키워드를 사용하여 메소드를 사용하는 사람에게 어떠한 예외가 처리되어야 하는지 쉽게 알 수 있도록하고, 호출한 메소드에게 예외를 전달하여 예외처리를 위임할 수 있다.

void method() throws Exception1, Exception2 {

}

위 코드를 보고 메소드 사용자는 Exception1, Exception2 예외가 발생할 수 있으니 해당 메소드를 호출한 족에서 처리해야 된다는 것을 바로 알 수 있다.

예외를 전달받은 메소드는 또다시 자신을 호출한 메소드에게 예외를 다시 전달할 수 있다. 이런 식으로 호출 스택에 있는 메소드들에게 계속해서 전달되다가 제일 마지막에 있는 main 메소드에서도 예외가 처리되지 않으면 프로그램이 비정상적으로 종료되게 된다.

그렇기 때문에 반드시 어디선가는 예외를 처리해주어야 한다.

public class Main {
    public static void main(String[] args) throws Exception{
        methodA();
    }

    static void methodA() throws Exception {
        methodB();
    }

    static void methodB() throws Exception {
        methodC();
    }

    static void methodC() throws Exception {
        throw new Exception("예외 발생!!");
    }
}

위의 코드의 실행 결과 예외가 main 메소드까지 전달되어 비정상적으로 프로그램이 종료된 것을 볼 수 있다.

Try-with-resources(JDK1.7~)

JDK1.7부터 try-with-resources문이 추가되어 입출력 관련 클래스에서 자원 해제를 보다 간결하게 할 수 있게 되었다.

FileInputStream fs = null;

try {
        fs = new FileInputStream("sample.txt");
} catch (FileNotFoundException e) {
        e.printStackTrace();
} finally {
        if (fs != null) {
            try {
                fs.close();
            } catch (IOException e) {
                    e.printStackTrace();
            }
        }
}

기존 자원 해제 방법인 위의 코드를 try-with-resources 문으로 바꾸면 다음과 같다.

try(FileInputStream fs = new FileInputStream("sample.txt")) {

} catch (IOException e) {
        e.printStackTrace();
}

try-with-resources문의 괄호() 안에 객체를 생성하는 코드를 넣으면, 이 객체는 close() 메소드를 따로 호출하지 않아도 try블록을 벗어날 때 자동으로 close() 메소드가 호출된다.

다만 try-with-resources문에 의해 자동으로 close() 메소드가 호출될 수 있으려면, AutoClosable 인터페이스를 구현해야 한다.

사용자 정의 예외 만들기

가능하면 기존의 예외 클래스를 활용하는 것이 좋지만, Exception클래스 또는 RuntimeException클래스를 상속받아 필요에 따라 알맞은 예외 클래스를 만들 수 있다.

간단한 예제를 작성해보면 다음과 같다.

public class MyException extends Exception{
    public MyException(String message) {
        super(message);
    }
}

연결된 예외(Chained exception)

한 예외가 다른 예외를 발생시킬 수도 있다. 예외 A가 예외 B를 발생시켰다면, A를 B의 '원인 예외(cause exception)'라고 한다.

try {
            throw new ExceptionA("A 예외 발생!!");
        } catch (ExceptionA exceptionA) {
            ExceptionB exceptionB = new ExceptionB("B 예외 발생!!");
            //initCause() 메소드를 사용하여 예외B의 원인 예외로 예외A를 등록
            exceptionB.initCause(exceptionA);
            throw exceptionB;
        }

결과를 보면 A예외에 의해 B예외가 발생했다고 출력되는 것을 볼 수 있다.

checkd예외를 unchecked예외로 바꾸어 예외 처리를 선택적으로 하도록 변경할 수도 있다.

void method() {
    //RuntimeException클래스 생성자를 사용하여 원인 예외 등록
    throw new RuntimeException(new Exception());
}

참고

  • 자바의 정석 3판(남궁성 저)

댓글