세상에 나쁜 코드는 없다

Java - 예외처리 정리 본문

Computer Science/Java

Java - 예외처리 정리

Beomseok Seo 2022. 5. 16. 20:52

예외처리 Exception Handling

프로그램의 오류

프로그램 에러/오류 : 프로그램이 실행 중 어떤 원인에 의해서 오작동을 하거나 비정상적으로 종료되는 경우

프로그램 에러 구분법 : 발생시점에 따라

  • 컴파일 에러(컴파일 타임 에러) : 컴파일 시 발생하는 에러
    • 소스코드 내의 오타나 잘못된 구문, 자료형 체크
  • 런타임 에러 : 실행 시에 발생하는 에러
    • 소스코드의 오류와 별개로 실행시에 생기는 오류
  • 논리적 에러 : 실행은 되지만, 의도와 다르게 동작하는 것

자바에서는 이중 런타임 에러를 Error 와 Exception으로 구분한다.

  • Error : OutOfMemoryError와 StackOverflowError 같이, 일단 발생하면 복구할 수 없는 심각한 오류
  • Exception : 문제가 생기더라도 코드로 수습할 수 있는 비교적 덜 심각한 오류

에러가 발생하면 프로그램이 비정상적으로 종료하지만, Exception이 발생하면 이에 대한 Handling 하는 코드를 미리 작성해 놓음으로써 프로그램의 비정상적인 종료를 막을 수 있다.


예외 클래스의 계층구조

자바에서는 실행시 발생할 수 있는 Exception과 Error을 클래스로 정의하였다.

모든 예외의 최고 조상은 Exception 클래스이며, Exception 클래스의 하위 클래스는 두 그룹으로 나눌 수 있다.

  • Exception 클래스와 그 자손들 : 주로 외부의 영향으로 발생하는 예외들
  • RuntimeException클래스와 그 자손들 : 주로 프로그래머의 실수로 의해 발생되는 예외들

RuntimeException 도 Exception클래스의 자손이지만 기능적으로 구분하기위해 위와 같이 나눈다.


try - catch 문

Exception의 경우 try-catch 를 통해 비정상적인 종료를 막을 수 있다.

  • try-catch 문의 구조

하나의 try 블럭 아래에는 여러 종류의 예외를 처리할 수 있도록 하나 이상의 catch 블럭이 올 수 있으며, 이 중 발생한 예외의 종류와 일치하는 단 한개의 catch 블럭만 수행된다. 블럭 검사는 위에서부터 진행된다.

try {
    // 예외가 발생할 가능성이 있는 문장들을 넣는다.
} catch (Exception1 e1) {
    // Exception1 이 발생했을 경우, 이를 처리하기 위한 로직이 들어간다.
} catch (Exception2 e2) {
    // Exception2 이 발생했을 경우, 이를 처리하기 위한 로직이 들어간다.
} catch (ExceptionN eN) {
    // ExceptionN 이 발생했을 경우, 이를 처리하기 위한 로직이 들어간다.
}
  • try 문에서 예외가 발생하지 않으면 catch 블럭을 건너뛰고 다음 로직을 진행한다.

  • try 문에서 예외가 발생한다면 catch 블럭 중 일치하는 예외를 찾게 되고 해당 블럭을 수행한후, try-catch 문을 빠져나간다.

  • try catch 블럭은 if문과 달리 내부의 문장이 하나뿐이어도 {}를 생략할 수 없다.

  • catch 블럭내의 변수는 catch 문 내의 지역변수이기 때문에, 위의 경우 모든 변수 이름을 "e"로 지정해도 무방하다. 물론 catch 문 내부의 catch 문이 있을 경우 변수명을 구별해 주어야 한다.

  • 모든 예외 클래스는 Exception 의 자손이므로, catch 블럭의 변수에 Exception클래스 타입의 참조변수를 선언하면 어떤 종류의 예외가 발생하더라도 처리할 수 있다.

예외클래스의 인스턴스에는 발생한 예외에 대한 정보가 담겨있는데, getMessage() 와 printStackTrace()를 통하여 정보들을 얻을 수 있다.

  • getMessage() : 예외클래스 인스턴스에 저장된 메시지를 얻을 수 있다.
  • printStackTrace() : 예외발생 당시 호출 스택에 있었던 메서드의 정보와 예외 메시지를 화면에 출력한다.
  • printStackTrace(PrintStream s) 나 printStackTrace(PrintWriter s) 를 사용하면 발생한 예외에 대한 정보를 파일로 저장할 수 있다.

JDK1.7 부터는 catch블럭에 '|' 를 사용하여, 여러개의 예외클래스를 하나의 catch 블럭으로 합칠 수 있다.

//멀티 캐치블럭의 예
try {
    ...
} catch  (ExceptionA | ExcpetionB | ExceptionC e) {
    e.printStackTrace();
}
  • 멀티 catch 블럭에 있는 예외클래스가 조상-자손 관계에 있다면 컴파일 에러가 발생한다.
  • 멀티 catch 블럭에서는 실제로 어떤 예외가 발생할 것인지 프로그래밍 과정에서 알 수 없기 때문에, '|' 기호로 연결된 예외 클래스들의 공통분모인 조상 예외클래스에 선언된 메서드만 사용할 수 있다.

예외 발생시키기

키워드 throw 를 사용하여 프로그래머가 고의로 예외를 발생시킬수 있다.

public static void main(String args[]) {
    try {
        Exception e = new Excpetion("고의로 발생시켰음");
        throw e;
        // throw new Exception("고의로 발생시켰음"); 
    } catch (Exception e) {
        System.out.println("에러메시지 : "+ e.getMessage());
    }
}

//실행결과 
//에러메시지 : 고의로 발생시켰음

Exception 과 컴파일

public static void main(String args[]) {
    throw new Exception();
}
public static void main(String args[]) {
    throw new RuntimeException();
}

Exception을 발생시킨 코드는 컴파일에러가 발생하고, RuntimeException 을 발생시킨 코드는 컴파일 에러가 생기지 않는다.

예외 클래스의 계층구조에서 구분했던 'Exception클래스와 자손들' 이 발생하는 경우는 예외처리를 해주지 않으면 컴파일 에러가 발생하고, 'RuntimeException클래스와 자손들' 이 발생하는 경우는 컴파일 에러가 발생하지 않는다.

RuntimeException클래스와 그 자손들에 해당하는 예외는 프로그래머의 실수로 발생하는 것이기 때문에 예외처리를 강제하지 않는다. 만약 예외처리가 강제된다면 예외처리를 해야하는 코드가 무지막지하게 늘어날 것이다. (모든 메서드 사용처에 NPE 예외처리를 해줘야 하는 등)

이러한 관점에서 컴파일러가 예외처리를 확인하지 않는 예외들은 'unchecked예외', 컴파일러가 예외처리를 강제하는 클래스는 'checked예외'라고 부른다.


메서드에 예외 선언하기

메서드에 예외를 선언하려면 메서드 선언부에 키워드 throws 를 사용해서 메서드내에서 발생할 수 있는 예외를 적어주면 된다. 예외가 여러개일 경우 쉼표로 구분한다.

    void method() throws Exception1, Exception2, ExceptionN {
        ...
    }

모든 예외의 최고조상 Exception 클래스를 메서드에 선언하면, 이 메서드는 모든 종류의 예외가 발생할 가능성이 있다는 뜻이다.

    void method() throws Exception {
        ...
    }

위와 같은 방식으로 예외를 선언하면, 프로그래밍시 고려하지 못한 Exception 까지 발생할 수 있다는 점에 주의해야한다.

자바에서 메서드를 작성할때 메서드 내에서 발생할 가능성이 있는 예외를 메서드에 명시해줌으로써 메서드를 사용하는 측에서 이에 대한 처리를 하도록 강요하기 때문에 프로그래머의 짐을 덜어줌과 동시에 보다 견고한 프로그램 코드를 작성할 수 있게 한다.

  • java api 문서를 통해 사용하고자 하는 메서드의 선언부를 보고 어떤 예외가 발생할 수 있으며 반드시 처리해주어야 하는 예외는 어떤것이 잇는지 확인하는 것이 좋다.

    메서드에 throws를 사용하여 예외를 선언하는것은 예외를 처리하는 것이 아니다. 해당 예외는 메서드를 호출한 메서드에 전달되므로 해당 메서드에서 예외를 처리해주는 것이 강제된다.

    class ExceptionTest {
     public static void main(String[] args) {
    
         try {
             method1(); //method1은 checked예외가 발생할 수 있는 메서드이므로 예외처리를 해주어야 한다.
         } catch (Exception e) {
             System.out.println(e.getMessage());
         }
    
     }
    
     static void method1() throws Exception {
         method2(); // method2는 checked예외가 발생할수 있는 메서드이므로 원래는 예외처리를 해주어야하지만, method1 선언부에 Exception을 throws 해줌으로써 Exception 을 method1을 호출한 부분으로 전달해주기 때문에 여기서는 별도의 예외처리를 해주지 않아도 된다.
     }
    
     statici void method2() throws Exception {
         throw new Exception("method2 Exception");
         //위 Exception 은 method2 에 선언된 throws Exception에 의해 method2를 호출한 메서드로 넘어가게 된다.
     }
    }
    

//실행 결과 :
//method2 Exception


<hr>

 ## finally 블럭

 finally 블럭은 try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, 예외의 발생여부와 상관없이 실행되어야 할 코드를 작성한다.

 ```java

    try {
        //예외가 발생할 수 잇는 가능성이 있는 코드
    } catch (Exception e) {
        //예외처리를 하는 코드
    } finally {
        //예외의 발생여부와 관계없이 항상 수행되어야 하는 코드
        //항상 try-catch 문의 맨 마지막에 위치
    }

예외가 발생한 경우 try - catch - finally 순으로, 발생하지 않은 경우 try - finally 순으로 실행된다.

  • try 문에서 return문이 실행되는 경우에도 return을 보류해두고, finally 블럭의 코드를 먼저 실행 한 후 return을 하게 된다.
  • 마찬가지로 catch 문에서 return이 발생해도 finally 구문은 실행된다.

자동 자원 반환 try-with-resources문

JDK1.7부터 try-catch 문의 변형인 try-with-resources문이 새로 추가되었다.

입출력과 관련된 클래스를 사용할때 사용한 후 꼭 닫아주어 자원을 반환받아야 하는 경우가 있는데 이 경우에 try-with-resources 를 사용하는 것이 유용하다.

    try {
        fis = new FileInputStream("score.dat");
        dis = new DataInputStream(fis);
        ...
    } catch (IOException ie) {
        ie.printStackTrace();
    } finally {
        dis.close(); //작업 중에 예외가 발생하더라도, dis가 닫히도록 finally 블럭에 넣음
    }

위와 같은 코드는 별 문제 없어 보일 수 있지만 dis.close() 문이 예외를 발생시킬수 있다는 점에서 문제가 있다. 그래서 코드를 수정하자면

    try {
        fis = new FileInputStream("score.dat");
        dis = new DataInputStream(fis);
        ...
    } catch (IOException ie) {
        ie.printStackTrace();
    } finally {
        try {
            if(dis != null)
                dis.close(); 
        } catch(IOException ie) {
            ie.printStackTrace();
        }
    }

와 같이 finally 문 내부에 try문이 또 들어가야 한다는 문제가 발생한다.(코드의 간결성이 떨어지고 보기 좋지 않다) 이런 경우를 개선하기 위하여 try-with-resources문이 추가되었다. try-with-resources 를 적용시킨 코드는 아래와 같다.

    // 괄호 안에 두 문장 이상 넣을 경우 ';'로 구분한다.
    try(FileInputStream fis = new FileInputStream("score.dat");
        DataInputStream dis = new DataInputStream(fis)) {
            ...
        } catch (IOException ie) {
            ie.printStackTrace();
        }

위와 같이 괄호 안에 객체를 생성하는 문장을 넣으면, 이 객체는 따로 close()를 호출하지 않아도 try 블럭을 벗어나는 순간 자동적으로 close()가 호출된다. 그 다음 catch나 finally 블럭이 수행된다.

  • try-with-resources 문에 의해 자동으로 객체의 close()가 호출될 수 있으려면 객체 클래스가 AutoCloseable이라는 인터페이스를 구현한 것이어야만 한다.

사용자정의 예외 만들기

필요하다면 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다. 보통 Exception클래스나 RuntimeException클래스로부터 상속받아 클래스를 작성한다.

class MyException extends Exception {
    MyException(String msg) {
        super(msg); // Exception클래스의 생성자 호출
    }
}
  • 기존의 예외클래스는 주로 Exception을 상속받아 checked예외로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아서 작성하는 쪽으로 바뀌어 가고 있다.

예외 되던지기 Exception re-throwing

한 메서드에서 발생할 수 있는 예외가 여럿인 경우, 몇 개는 메서드 내에서 자체적으로 처리하고, 나머지는 선언부에 지정하여 호출한 메서드에서 처리하도록 함으로써, 양쪽에서 나눠서 처리되도록 할 수 있다.

프로그래밍을 하다 보면 하나의 예외에 대해서 예외가 발생한 메서드와 호출한 메서드 양쪽 모두에서 처리해줘야 할 작업이 있을 수 있다.

자바에서는 단 하나의 예외에 대해서도 양쪽에서 처리할 수 있게 할 수 있다.

이것은 예외를 처리한 후에 인위적으로 다시 발생시키는 방법을 통해서 가능한데, 이것을 '예외 되던지기'라고 한다.


    public static void main(String args[]) {
        try {
            method1();
        } catch (Exception e) {
            //Exception 예외처리
        }
    }

    static void method1() throws Exception {
        try {
            throw new Exception();
        } catch (Exception e) {
            //Exception 예외처리
            throw e; // 예외 다시 발생시키기
        }
    }

'Computer Science > Java' 카테고리의 다른 글

Generic Programming  (2) 2024.04.10
상속이 안티패턴이라고?  (0) 2024.03.17
[Java] Generics, Annotation, Enum  (0) 2022.05.26