Programming Language/Java

[Java] 15. 예외 처리(Exception handling)(4) - finally블럭, 자동 자원 반환: try-with-resources문, 사용자 정의 예외

lumana 2024. 7. 9. 05:39

예외처리(4) - finally블럭, 자동 자원 반환: try-with-resources문, 사용자 정의 예외

finally블럭

  • finally 블럭은 try-catch문과 함께 예외의 발생여부에 상관없이 실행되어야할 코드를 포함시킬 목적으로 사용된다.

  • try-catch문의 끝에 선택적으로 덧붙여 사용할 수 있으며, try-catch-finally 순서로 구성된다

    • 예외 발생시 : try->catch->finally

      • 예외 발생 X : try->finally
class FinallyTest {
    public static void main(String args[]) {
        try {
            startInstall();            // 프로그램 설치에 필요한 준비를 한다.
            copyFiles();            // 파일들을 복사한다.
            deleteTempFiles();        // 프로그램 설치에 사용된 임시파일들을 삭제한다.
        } catch (Exception e) {
            e.printStackTrace();
            deleteTempFiles();        // 프로그램 설치에 사용된 임시파일들을 삭제한다.
        }    // try-catch의 끝
    }    // main의 끝

    static void startInstall() { /* 프로그램 설치에 필요한 준비를 하는 코드를 적는다. */ }

    static void copyFiles() { /* 파일들을 복사하는 코드를 적는다. */ }

    static void deleteTempFiles() { /* 임시파일들을 삭제하는 코드를 적는다. */ }
}
  • deleteTempFiles()메서드는 예외의 발생 여부와 관계없이 실행되어야 하는 것이다.

    • 따라서 아래 코드처럼 finally블럭을 사용하는 것이 좋다.
class FinallyTest2 {
    public static void main(String args[]) {
        try {
            startInstall();            // 프로그램 설치에 필요한 준비를 한다.
            copyFiles();            // 파일들을 복사한다.
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            deleteTempFiles();        // 프로그램 설치에 사용된 임시파일들을 삭제한다.
        }    // try-catch의 끝
    }    // main의 끝

    static void startInstall() { /* 프로그램 설치에 필요한 준비를 하는 코드를 적는다. */ }

    static void copyFiles() { /* 파일들을 복사하는 코드를 적는다. */ }

    static void deleteTempFiles() { /* 임시파일들을 삭제하는 코드를 적는다. */ }
}

Example 8.19

class FinallyTest3 {
    public static void main(String args[]) {
        // method1()은 static메서드이므로 인스턴스 생성없이 직접 호출 가능하다.
        FinallyTest3.method1();
        System.out.println("method1()의 수행을 마치고 main메서드로 돌아왔습니다.");
    }    // main의 끝

    static void method1() {
        try {
            System.out.println("method1()이 호출되었습니다.");
            return;        // 현재 실행 중인 메서드를 종료한다.
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("method1()의 finally블럭이 실행되었습니다.");
        }
    }    // method1메서드의 끝
}
  • try 블럭에서 return문이 실행되는 경우에도 finally블럭의 문장들이 먼저 실행된 후에, 현재 실행 중인 메서드를 종료한다.

    • 이와 마찬가지로 catch블럭의 문장 수행 중에 return문을 만나도 finally블럭의 문장들은 수행된다.

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

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

    • I/O 관련 클래스를 사용할 때 유용하다.
  • 입출력에 사용되는 클래스 중에서는 사용한 후에 꼭 닫아 줘야 하는 것들이 있다.

    • 닫아줘야 자원이 반환되기 때문이다.
try {
        fis = new FileInputStream("score.dat");
        dis = new DataInputStream(fis);
        ...
} catch (IOException ie) {
        ie.printStackTrace();
} finally {
        dis.close(); // 작업 중에 예외가 발생하더라도, dis가 닫히도록 finally블록에 넣는다.
}
  • finally블록 안에 close()를 넣었는데, 진짜 문제는 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-catch블록을 추가하는 것은 보기에도 안좋은데 더 큰 문제가 있다.

    • try블럭과 finally블럭에서 모두 예외가 발생하면 try 블럭의 예외는 무시된다. 결과적으로 try블럭에서 예외가 발생했는지 조차 모를 수 있게 된다
  • 이러한 점을 개선하기 위해 try-with-resources문이 등장했다.

try (fis = new FileInputStream("score.dat");
        dis = new DataInputStream(fis)) { // 괄호() 안에 두 문장 이상 넣을 경우 ';'로 구분한다
                while(true) {
                        // 생략
                }
} catch (EOFException e) {
        // 생략
} catch (IOException ie) {
        // 생략
} // try
  • try-with-resources문의 괄호() 안에 객체를 생성하는 문장을 넣으면 따로 close()를 호출하지 않아도 try블럭을 벗어나는 순간 자동적으로 close()가 호출된다.

    • 그 다음에 catch 또는 finally블럭이 수행된다

    • 참고) try블럭의 괄호() 안에 변수를 선언하는 것도 가능하다. (try 블럭 내에서만 사용 가능)

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

public interface AutoCloseable {
        void close() throws Exception;
}
  • 아까 위에서 try블럭과 finally블럭에서 모두 예외가 발생하면 try 블럭의 예외는 무시된다고 하였다.

    • 만약 try-with-resources에서 호출된 close()에서 예외가 발생하면 어떻게 될까?
class TryWithResourceEx {
    public static void main(String args[]) {
        try (CloseableResource cr = new CloseableResource()) {
            cr.exceptionWork(false);    // 예외가 발생하지 않는다.
        } catch(WorkException e) {
            e.printStackTrace();
        } catch(CloseException e) {
            e.printStackTrace();
        }
        System.out.println();

        try (CloseableResource cr = new CloseableResource()) {
            cr.exceptionWork(true);        // 예외가 발생한다.
        } catch(WorkException e) {
            e.printStackTrace();
        } catch(CloseException e) {
            e.printStackTrace();
        }
    }    // main의 끝
}

class CloseableResource implements AutoCloseable {
    public void exceptionWork(boolean exception) throws WorkException {
        System.out.println("exceptionWork(" + exception + ")가 호출됨");

        if (exception)
            throw new WorkException("WorkException발생!!!");
    }

    public void close() throws CloseException {
        System.out.println("close()가 호출됨");
        throw new CloseException("CloseException발생!!!");
    }
}

class WorkException extends Exception {
    WorkException(String msg) { super(msg); }
}

class CloseException extends Exception {
    CloseException(String msg) { super(msg); }
}
  • try-catch 첫 번째에서는 close()에서만 예외를 발생시킨다.

  • try-catch 두 번째에서는 exceptionWork(), close() 모두 예외를 발생시킨다

    • 두 예외가 동시에 발생할 수 없기 때문에, 실제 발생한 예외를 WorkException으로 하고, CloseException은 억제된 예외로 다룬다.

      • 억제된 예외에 대한 정보는 실제로 발생한 예외에 저장된다

사용자 정의 예외 만들기

  • 기존에 정의된 예외 클래스 말고도 필요에 따라 프로그래머가 새로운 예외 클래스를 정의하여 사용할 수 있다.

  • 보통 Exception 클래스로부터 상속받는 클래스를 만들지만, 필요에 따라 알맞는 예외 클래스를 선택할 수 있다.

class MyException extends Exception {
        MyException(String msg) { // 문자열을 매개변수로 받는 생성자
                super(msg); // 조상인 Exception클래스의 생성자를 호출한다.
        }
}
  • 필요하다면 멤버 변수나 메서드를 추가할 수 있다.

  • Exception클래스는 생성자에서 String값을 받아 메시지를 저장할 수 있다.


  • 기존의 예외 클래스는 Exception을 상속받아서 'checked예외'로 작성하는 경우가 많았지만, 요즘은 예외처리를 선택적으로 할 수 있도록 RuntimeException을 상속받아서 작성하는 쪽으로 바뀌어가고 있다.

    • 'check 예외'는 반드시 예외처리를 해줘야 해서 불필요한 경우에도 try-catch를 넣어야 해서 코드가 복잡해지기 때문

참조) Java의 정석 3rd edition(남궁성, 도우출판)