2. Java Tutorials

[Java Tutorials] #22 Lesson: Exceptions

Dorothy. 2024. 9. 30. 11:31

안녕하세요, Dorothy입니다.

오늘은 22강 예외(Exceptions)에 대해서 배워보겠습니다.


 

[튜토리얼]

자바 프로그래밍 언어는 오류 및 기타 예외적인 이벤트를 처리하기 위해 예외를 사용합니다. 이 레슨에서는 예외를 언제 그리고 어떻게 사용하는지 설명합니다.

 

 1. What Is an Exception? 
예외(Exception)는 정상적인 명령 흐름을 방해하는 프로그램 실행 중에 발생하는 이벤트입니다.


1) The Catch or Specify Requirement
이 섹션에서는 예외를 catch하고 처리하는 방법을 다룹니다. 이 논의는 try, catch, finally 블록뿐만 아니라 chain exception와 logging을 포함합니다.

2) How to Throw Exceptions
이 섹션에서는 throw 스테이트먼트와 Throwable 클래스 및 그 하위 클래스를 다룹니다.

3) The try-with-resources Statement
이 섹션에서는 하나 이상의 리소스를 선언하는 try statement인 try-with-resources 스테이트먼트에 대해 설명합니다. 리소스는 프로그램이 사용을 마친 후 반드시 닫아야(close) 하는 객체입니다. try-with-resources statement은 statement가 끝날 때마다 각 리소스가 닫히도록 보장합니다.

4) Unchecked Exceptions — The Controversy
이 섹션에서는 RuntimeException의 하위 클래스로 표시된 체크되지 않은 예외[unchecked exception]의 올바른 사용법과 잘못된 사용법을 설명합니다.

5) Advantages of Exceptions
예외를 사용하여 error를 관리하는 것은 전통적인 오류 관리 기술에 비해 몇 가지 장점이 있습니다. 이 섹션에서 더 자세히 배울 수 있습니다.

 

 2. 예외(exception) : 정의 

용어 예외(exception)는 "예외적인 사건[exceptional event]"이라는 말의 줄임말입니다.


정의: 예외(exception)는 프로그램 실행 중에 발생하여 프로그램 정상적인 명령 흐름을 방해하는 이벤트입니다.


메서드 내에서 오류가 발생하면, 메서드는 특정 객체를 생성하여 런타임 시스템에 전달합니다. 예외 객체[exception object]라고 불리는 이 객체는 에러에 대한 정보, 에러가 발생했을 때의 프로그램 상태 등을 포함합니다. 예외 객체를 생성하여 런타임 시스템에 전달하는 것을 예외를 던진다[throwing an exception]고 합니다.

 

메서드가 예외를 던진 후, 런타임 시스템은 이를 처리할 무언가를 찾으려고 시도합니다. 예외를 처리할 수 있는 가능한 "무언가"의 집합은 에러가 발생한 메서드에 도달하기 위해 호출된 메서드의 순서 있는 목록입니다. 이 메서드 목록은 호출 스택(call stack)이라고 불립니다 (다음 그림을 참조하십시오).

1) The Call Stack

 

런타임 시스템은 호출 스택을 검색하여 예외를 처리할 수 있는 코드 블록을 포함하는 메서드를 찾습니다. 이 코드 블록을 예외 핸들러(exception handler)라고 합니다. 검색은 에러가 발생한 메서드부터 시작하여 메서드가 호출된 역순으로 호출 스택을 통해 진행됩니다. 적절한 핸들러가 발견되면, 런타임 시스템은 해당 예외를 이 핸들러에게 전달합니다. 이 예외 핸들러는, 던져진 예외 객체의 타입이 이 핸들러가 처리할 수 있는 타입과 일치하는 경우 적절한 핸들러로 간주됩니다. 

 

선택된 예외 핸들러는 예외를 "잡는다[catch]"고 합니다. 만약 런타임 시스템이 호출 스택의 모든 메서드를 철저히 검색했음에도 적절한 예외 핸들러를 찾지 못하면, 다음 그림과 같이 런타임 시스템(결과적으로 프로그램)은 종료됩니다.

Searching the call stack for the exception handler.

 

예외를 사용하여 에러를 관리하는 것은 전통적인 에러 관리 기술에 비해 몇 가지 장점이 있습니다. 자세한 내용은 예외의 장점(Advantages of Exceptions) 섹션에서 배울 수 있습니다.

 

 

2) The Catch or Specify Requirement

유효한 자바 프로그래밍 언어 코드에서는 Catch 또는 Specify 요구 사항을 준수해야 합니다. 이는 특정 예외를 발생시킬 수 있는 코드를 다음 중 하나로 감싸야 함을 의미합니다:

  • 예외를 잡는 try statement. try는 예외를 처리하는 핸들러를 제공해야 합니다. 이는 "Catching and Handling Exceptions."에서 설명한 대로입니다.
  • 예외를 발생시킬 수 있음을 지정하는 메서드. 메서드는 "Specifying the Exceptions Thrown by a Method"에서 설명한 대로 예외를 나열하는 throws 절을 제공해야 합니다.

Catch 또는 Specify 요구 사항을 준수하지 않는 코드는 컴파일되지 않습니다.

모든 예외가 Catch 또는 Specify 요구 사항의 대상이 되는 것은 아닙니다. 왜 그런지 이해하려면, 세 가지 기본 예외 범주를 살펴볼 필요가 있으며, 이 중 하나만이 요구 사항의 대상입니다.

 

 3. The Three Kinds of Exceptions 

첫 번째 유형의 예외는 체크 예외[checked exception]입니다. 이는 잘 작성된 애플리케이션이 예외를 예상하고 복구해야 하는 예외적인 조건입니다. 예를 들어, 애플리케이션이 사용자에게 입력 파일 이름을 묻고, 그 이름을 java.io.FileReader 생성자에 전달하여 파일을 여는 경우를 생각해봅시다. 일반적으로 사용자는 기존의 읽을 수 있는 파일 이름을 제공하므로 FileReader 객체의 생성이 성공하고 애플리케이션 실행이 정상적으로 진행됩니다. 그러나 때때로 사용자가 존재하지 않는 파일 이름을 제공하면 생성자는 java.io.FileNotFoundException을 던집니다. 잘 작성된 프로그램은 이 예외를 잡아 사용자에게 실수를 알리고, 수정된 파일 이름을 요청할 수 있습니다.

checked exception는 Catch 또는 Specify 요구 사항의 대상입니다. Error, RuntimeException 및 그 하위 클래스로 표시된 예외를 제외한 모든 예외는 checked exception입니다.

 

두 번째 유형의 예외는 에러[error]입니다. 이는 애플리케이션 외부의 예외적인 조건으로, 애플리케이션이 일반적으로 예측하거나 복구할 수 없는 상황입니다. 예를 들어, 애플리케이션이 파일을 성공적으로 열었지만 하드웨어 또는 시스템 고장으로 인해 파일을 읽을 수 없는 경우를 생각해봅시다. 읽기에 실패하면 java.io.IOError가 발생합니다. 애플리케이션은 이 예외를 잡아 사용자에게 문제를 알릴 수 있지만, 프로그램이 스택 트레이스를 출력하고 종료하는 것이 더 합리적일 수도 있습니다.

 

에러는 Catch 또는 Specify 요구 사항의 대상이 아닙니다. 에러는 Error 및 그 하위 클래스로 표시된 예외를 의미합니다.

 

세 번째 유형의 예외는 런타임 예외입니다. 이는 애플리케이션 내부의 예외적인 조건으로, 애플리케이션이 일반적으로 예측하거나 복구할 수 없는 상황입니다. 이러한 예외는 주로 논리 오류나 API의 부적절한 사용과 같은 프로그래밍 버그를 나타냅니다. 예를 들어, 이전에 설명한 파일 이름을 FileReader 생성자에 전달하는 애플리케이션을 생각해봅시다. 논리 오류로 인해 null이 FileReader 생성자에 전달되면, 이 생성자는 NullPointerException을 던질 것입니다. 애플리케이션은 이 예외를 잡을 수 있지만, 예외를 발생시킨 버그를 제거하는 것이 더 합리적일 것입니다.

런타임 예외는 Catch 또는 Specify 요구 사항의 대상이 아닙니다. 런타임 예외는 RuntimeException 및 그 하위 클래스로 표시된 예외를 의미합니다.

에러와 런타임 예외는 둘 다, 체크되지 않은 예외[unchecked exception]라고 합니다.

 

 

1) Bypassing Catch or Specify

일부 프로그래머는 Catch 또는 Specify 요구 사항을 예외 메커니즘의 심각한 결함으로 간주하고, checked exception 대신 unchecked exception를 사용하여 이를 우회합니다. 일반적으로 이는 권장되지 않습니다. (Unchecked Exceptions — The Controversy[논란]) 섹션에서는 unchecked exception를 사용하는 것이 어떤 경우에 적절한지를 설명합니다.

 

2) Catching and Handling Exceptions

이 섹션에서는 세 가지 예외 핸들러 구성 요소인 try, catch, finally 블록을 사용하여 예외 핸들러를 작성하는 방법을 설명합니다. 그런 다음 Java SE 7에서 도입된 try-with-resources statement에 대해 설명합니다. try-with-resources statement은 스트림과 같은 Closeable 리소스를 사용하는 상황에 특히 적합합니다.

이 섹션의 마지막 부분에서는 예제를 통해 다양한 시나리오에서 발생하는 상황을 분석합니다.

다음 예제는 ListOfNumbers라는 클래스를 정의하고 구현합니다. ListOfNumbers는 생성 시, 0부터 9까지의 순차적인 값으로 10개의 Integer 엘리먼트를 포함하는 ArrayList를 생성합니다. ListOfNumbers 클래스는 또한 writeList라는 메서드를 정의하며, 이 메서드는 숫자 리스트를 OutFile.txt라는 텍스트 파일에 씁니다. 이 예제에서는 Basic I/O에서 다루는 java.io에 정의된 출력 클래스를 사용합니다.

// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class ListOfNumbers {

    private List<Integer> list;
    private static final int SIZE = 10;

    public ListOfNumbers () {
        list = new ArrayList<Integer>(SIZE);
        for (int i = 0; i < SIZE; i++) {
            list.add(new Integer(i));
        }
    }

    public void writeList() {
	// The FileWriter constructor throws IOException, which must be caught.
        PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));

        for (int i = 0; i < SIZE; i++) {
            // The get(int) method throws IndexOutOfBoundsException, which must be caught.
            out.println("Value at: " + i + " = " + list.get(i));
        }
        out.close();
    }
}

 

writeList 메서드의 첫번째 라인 코드에 포함되어 있는 아래 코드는
new FileWriter("OutFile.txt")

 

FileWriter 생성자 호출입니다. 이 생성자는 파일에 대한 출력 스트림을 초기화합니다. 파일을 열 수 없는 경우, 생성자는 IOException을 던집니다.

그리고 writeList 메서드의 또 다른 아래 코드는,

list.get(i)

 

ArrayList 클래스의 get 메서드 호출입니다. 이 메서드는 아규먼트의 값이 너무 작거나(0보다 작음) 너무 큰 경우(현재 ArrayList에 포함된 엘리먼트 개수보다 많음) IndexOutOfBoundsException을 던집니다.

ListOfNumbers 클래스를 컴파일하려고 하면, 컴파일러는 FileWriter 생성자가 던지는 예외에 대한 에러 메시지를 출력합니다. 그러나 get 메서드가 던지는 예외에 대한 에러 메시지는 표시되지 않습니다. 그 이유는 FileWriter 생성자가 던지는 예외인 IOException은 checked exception이고, get 메서드가 던지는 예외인 IndexOutOfBoundsException은 unchecked exception이기 때문입니다.


이제 ListOfNumbers 클래스와 그 안에서 예외가 발생할 수 있는 위치에 익숙해졌으므로, 해당 예외를 잡고 처리할 예외 핸들러를 작성할 준비가 되었습니다.

public void writeList() throws IOException {
	// The FileWriter constructor throws IOException, which must be caught.
     PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));

     for (int i = 0; i < SIZE; i++) {
         // The get(int) method throws IndexOutOfBoundsException, which must be caught.
         out.println("Value at: " + i + " = " + list.get(i));
     }
     out.close();
 }

 

 

3) The try Block

예외 처리기를 작성하는 첫 번째 단계는 예외를 발생시킬 수 있는 코드를 try 블록 내에 포함하는 것입니다. 일반적으로 try 블록은 다음과 같이 생겼습니다:

try {
    code ...
}
catch and finally blocks . . .

 

예제에서 code라고 표시된 부분에는 예외를 발생시킬 수 있는 하나 이상의 적법한 코드 라인들이 포함됩니다. (catch 및 finally 블록은 다음 두 하위 섹션에서 설명합니다.)

ListOfNumbers 클래스의 writeList 메서드에 대한 예외 핸들러를 작성하려면 writeList 메서드의 예외 발생 statement을 try 블록 내에 포함하십시오. 이를 수행하는 방법은 여러 가지가 있습니다. 예외를 발생시킬 수 있는 각 코드 라인을 자체 try 블록 내에 넣고 각각에 대해 별도의 예외 핸들러를 제공할 수 있습니다. 또는 모든 writeList 코드를 단일 try 블록 내에 넣고 여러 핸들러를 연관시킬 수 있습니다. 다음 목록은 전체 메서드에 대해 하나의 try 블록을 사용합니다. 왜냐하면 문제의 코드가 매우 짧기 때문입니다.

private List<Integer> list;
private static final int SIZE = 10;

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entered try statement");
        FileWriter f = new FileWriter("OutFile.txt");
        out = new PrintWriter(f);
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    }
    catch and finally blocks  . . .
}

 

try 블록 내에서 예외가 발생하면, 해당 예외는 관련된 예외 핸들러가 처리합니다. 예외 핸들러를 try 블록과 연관시키려면, 그 뒤에 catch 블록을 넣어야 합니다. 다음 섹션인 "catch 블록"에서 그 방법을 설명합니다.

 

 

4) The catch Blocks

예외 핸들러를 try 블록과 연관시키려면, try 블록 바로 뒤에 하나 이상의 catch 블록을 제공해야 합니다. try 블록의 끝과 첫 번째 catch 블록의 시작 사이에는 코드가 있을 수 없습니다.

try {

} catch (ExceptionType name) {

} catch (ExceptionType name) {

}

 

각 catch 블록은 그 아규먼트로 표시된 타입[ ex) (Exception name) ]의 예외를 처리하는 예외 핸들러입니다. 아규먼트 타입인 ExceptionType은 처리할 수 있는 예외의 타입을 선언하며, 반드시 Throwable 클래스를 상속하는 클래스의 이름이어야 합니다. 핸들러는 name 변수로 예외를 참조할 수 있습니다.

catch 블록에는 예외 핸들러가 호출될 때 실행되는 코드가 포함됩니다. 런타임 시스템은 핸들러의 ExceptionType이 던져진 예외의 타입과 일치하는 호출 스택에서 첫 번째 핸들러일 때 해당 예외 핸들러를 호출합니다. 던져진 객체가 예외 핸들러의 아규먼트로 합법적으로 할당될 수 있는 경우 시스템은 이를 일치하는 것으로 간주합니다.

 

다음은 writeList 메서드에 대한 두 가지 예외 핸들러입니다:

try {

} catch (IndexOutOfBoundsException e) {
    System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
    System.err.println("Caught IOException: " + e.getMessage());
}

 

예외 핸들러는 단순히 에러 메시지를 출력하거나 프로그램을 중단하는 것 이상을 할 수 있습니다. 예외 핸들러는 에러 복구를 수행하거나 사용자에게 결정을 요청하거나, chained exception를 사용하여 에러를 상위 핸들러로 전달할 수 있습니다. 연쇄 예외에 대해서는 "Chained Exceptions" 섹션에서 설명합니다.

 

5) Catching More Than One Type of Exception with One Exception Handler

Java SE 7 및 이후 버전에서는 단일 catch 블록이 여러 타입의 예외를 처리할 수 있습니다. 이 기능은 코드 중복을 줄이고 지나치게 광범위한 예외를 catch하는 유혹을 줄일 수 있습니다.

catch 절에서 해당 블록이 처리할 수 있는 예외 타입을 지정하고, 각 예외 타입을 수직선[vertical bar( | )]으로 구분합니다:

catch (IOException|SQLException ex) {
    logger.log(ex);
    throw ex;
}

 

참고: catch 블록이 두 개 이상의 예외 타입을 처리하는 경우, catch 파라미터는 암시적으로 final입니다. 이 예에서 catch 파라미터 ex는 final이므로 catch 블록 내에서 그 값을 변경할 수 없습니다.

 

6) The finally Block

finally 블록은 try 블록이 종료될 때 항상 실행됩니다. 이는 예기치 않은 예외가 발생하더라도 finally 블록이 실행되도록 보장합니다. 그러나 finally는 예외 처리뿐만 아니라 리턴(return), 계속(continue), 또는 중단(break)에 의해 cleanup 코드(리소스를 close하는)가 우연히 건너뛰는 것을 방지하는 데에도 유용합니다. cleanup 코드를 finally 블록에 넣는 것은 예외가 예상되지 않는 경우에도 항상 좋은 관행입니다.

참고: try 또는 catch 코드가 실행되는 동안 JVM이 종료되면 finally 블록은 실행되지 않을 수 있습니다.

 

여기서 작업한 writeList 메서드의 try 블록은 PrintWriter를 오픈합니다. 프로그램은 writeList 메서드를 종료하기 전에 해당 스트림을 닫아야 합니다. 이것은 다소 복잡한 문제를 제기하는데, 그 이유는 writeList의 try 블록이 세 가지 방법 중 하나로 종료될 수 있기 때문입니다.

1. 새로운 FileWriter statement가 실패하고 IOException을 던집니다.
2. list.get(i) statement가 실패하고 IndexOutOfBoundsException을 던집니다.
3. 모든 것이 성공하고 try 블록이 정상적으로 종료됩니다.


런타임 시스템은 try 블록 내에서 무슨 일이 일어나든 상관없이 finally 블록 내의 문장을 항상 실행합니다. 따라서 cleanup를 수행하기에 완벽한 장소입니다.

 

다음은 writeList 메서드의 finally 블록으로, PrintWriter와 FileWriter를 정리한 후 닫습니다.

finally {
    if (out != null) { 
        System.out.println("Closing PrintWriter");
        out.close(); 
    } else { 
        System.out.println("PrintWriter not open");
    } 
    if (f != null) {
	    System.out.println("Closing FileWriter");
	    f.close();
	}	
}

 


중요: 파일을 닫거나 리소스를 해제할 때는 finally 블록 대신 try-with-resources 문을 사용하세요. 다음 예제는 try-with-resources 문을 사용하여 writeList 메서드의 PrintWriter와 FileWriter를 정리하고 닫습니다.

public void writeList() throws IOException {
    try (FileWriter f = new FileWriter("OutFile.txt")) {
         PrintWriter out = new PrintWriter(f)) {
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    }
}

 

try-with-resources statement은 더 이상 필요하지 않은 시스템 리소스를 자동으로 해제합니다. 자세한 내용은 "try-with-resources 문"을 참조하십시오.


 

7) The try-with-resources Statement

try-with-resources statement은 하나 이상의 리소스를 선언하는 try 문입니다. 리소스는 프로그램이 사용을 마친 후 반드시 닫아야(close) 하는 객체입니다. try-with-resources statement은 각 리소스가 statement가 끝날 때 닫히도록 보장합니다. java.lang.AutoCloseable을 구현하는 모든 객체(여기에는 java.io.Closeable을 구현하는 모든 객체가 포함됩니다)는 리소스로 사용할 수 있습니다.

다음 예제는 파일에서 첫 번째 라인을 읽습니다. 이 예제는 FileReader와 BufferedReader 인스턴스를 사용하여 파일에서 데이터를 읽습니다. FileReader와 BufferedReader는 프로그램이 사용을 마친 후 반드시 닫아야 하는 리소스입니다:

static String readFirstLineFromFile(String path) throws IOException {
    try (FileReader fr = new FileReader(path);
         BufferedReader br = new BufferedReader(fr)) {
        return br.readLine();
    }
}

 

이 예제에서 try-with-resources statement에 선언된 리소스는 FileReader와 BufferedReader입니다. 이 리소스들의 선언문은 try 키워드 바로 뒤의 괄호 안에 나타납니다. Java SE 7 이후의 FileReader와 BufferedReader 클래스는 java.lang.AutoCloseable 인터페이스를 구현합니다. FileReader와 BufferedReader 인스턴스가 try-with-resources statement에 선언되었기 때문에, BufferedReader.readLine 메서드가 IOException을 던져 try statement이 정상적으로 완료되든 갑작스럽게 완료되든 상관없이 닫힙니다.

Java SE 7 이전에는 try statement가 정상적으로 완료되든 갑작스럽게 완료되든 상관없이 리소스가 닫히도록 보장하기 위해 finally 블록을 사용할 수 있습니다. 다음 예제는 try-with-resources 문 대신 finally 블록을 사용합니다:

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    try {
        return br.readLine();
    } finally {
        br.close();
        fr.close();
    }
}

 

그러나 이 예제는 리소스 누수가 발생할 수 있습니다. 프로그램은 리소스 사용이 끝난 후 가비지 컬렉터(GC)가 리소스의 메모리를 회수하는 것에만 의존해서는 안 됩니다. 프로그램은 리소스를 운영 체제로 반환해야 하며, 일반적으로 리소스의 close 메서드를 호출하여 이를 수행합니다. 그러나 GC가 리소스를 회수하기 전에 프로그램이 이를 수행하지 못하면 리소스를 해제하는 데 필요한 정보가 손실됩니다. 운영 체제가 여전히 사용 중으로 간주하는 리소스가 누수된 것입니다.

이 예제에서 readLine 메서드가 예외를 던지고, finally 블록의 br.close() 문이 예외를 던지면 FileReader가 누수될 수 있습니다. 따라서 프로그램의 리소스를 닫기 위해 finally 블록 대신 try-with-resources 문을 사용하십시오.

readLine 메서드와 close 메서드가 모두 예외를 던지면, 메서드 readFirstLineFromFileWithFinallyBlock은 finally 블록에서 던져진 예외를 던집니다. try 블록에서 던져진 예외는 억제됩니다. 반면, 예제 readFirstLineFromFile에서 try 블록과 try-with-resources 문에서 모두 예외가 발생하면, 메서드 readFirstLineFromFile은 try 블록에서 던져진 예외를 던집니다. try-with-resources 블록에서 던져진 예외는 억제됩니다. Java SE 7 이후 버전에서는 억제된 예외를 검색할 수 있습니다. 자세한 내용은 "억제된 예외(Suppressed Exceptions)" 섹션을 참조하십시오.

 

다음 예제는 zip 파일 zipFileName에 포함된 파일 이름을 가져와서 이러한 파일 이름을 포함하는 텍스트 파일을 생성합니다:

public static void writeToFileZipFileContents(String zipFileName,
                                           String outputFileName)
                                           throws java.io.IOException {

    java.nio.charset.Charset charset =
         java.nio.charset.StandardCharsets.US_ASCII;
    java.nio.file.Path outputFilePath =
         java.nio.file.Paths.get(outputFileName);

    // Open zip file and create output file with 
    // try-with-resources statement

    try (
        java.util.zip.ZipFile zf =
             new java.util.zip.ZipFile(zipFileName);
        java.io.BufferedWriter writer = 
            java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
    ) {
        // Enumerate each entry
        for (java.util.Enumeration entries =
                                zf.entries(); entries.hasMoreElements();) {
            // Get the entry name and write it to the output file
            String newLine = System.getProperty("line.separator");
            String zipEntryName =
                 ((java.util.zip.ZipEntry)entries.nextElement()).getName() +
                 newLine;
            writer.write(zipEntryName, 0, zipEntryName.length());
        }
    }
}

 

이 예제에서 try-with-resources 문은 세미콜론으로 구분된 두 개의 선언, 즉 ZipFile과 BufferedWriter를 포함합니다. 이를 직접 따르는 코드 블록이 정상적으로 종료되든 예외로 인해 종료되든, BufferedWriter와 ZipFile 객체의 close 메서드가 이 순서로 자동으로 호출됩니다. 리소스의 close 메서드는 생성된 순서의 반대로 호출된다는 점에 유의하십시오.

 

다음 예제는 try-with-resources 문을 사용하여 java.sql.Statement 객체를 자동으로 닫습니다:

public static void viewTable(Connection con) throws SQLException {

    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";

    try (Statement stmt = con.createStatement()) {
        ResultSet rs = stmt.executeQuery(query);

        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");

            System.out.println(coffeeName + ", " + supplierID + ", " + 
                               price + ", " + sales + ", " + total);
        }
    } catch (SQLException e) {
        JDBCTutorialUtilities.printSQLException(e);
    }
}

 

이 예제에서 사용된 java.sql.Statement 리소스는 JDBC 4.1 및 이후 API의 일부입니다.

참고: try-with-resources statement은 일반 try 문처럼 catch 및 finally 블록을 가질 수 있습니다. try-with-resources statement에서는 선언된 리소스가 닫힌 후에 catch 또는 finally 블록이 실행됩니다.

 

 4. Suppressed Exceptions 

try-with-resources 스테이먼트와 관련된 코드 블록에서 예외가 발생할 수 있습니다. 예제 writeToFileZipFileContents에서 try 블록에서 예외가 발생할 수 있으며, ZipFile 및 BufferedWriter 객체를 닫으려고 할 때 try-with-resources 문에서 최대 두 개의 예외가 발생할 수 있습니다. 만약 try 블록에서 예외가 발생하고 try-with-resources 문에서도 하나 이상의 예외가 발생하면, try-with-resources 문에서 발생한 예외는 억제되고, writeToFileZipFileContents 메서드에서 던져지는 예외는 try 블록에서 발생한 예외입니다. 이러한 억제된 예외는 try 블록에서 발생한 예외의 Throwable.getSuppressed 메서드를 호출하여 검색할 수 있습니다.

 

1) Classes That Implement the AutoCloseable or Closeable Interface

AutoCloseable 및 Closeable 인터페이스를 구현하는 클래스 목록은 해당 인터페이스의 Javadoc을 참조하십시오. Closeable 인터페이스는 AutoCloseable 인터페이스를 확장합니다. Closeable 인터페이스의 close 메서드는 IOException 유형의 예외를 던지는 반면, AutoCloseable 인터페이스의 close 메서드는 Exception 유형의 예외를 던집니다. 따라서 AutoCloseable 인터페이스의 하위 클래스는 close 메서드의 동작을 재정의하여 IOException과 같은 특화된 예외를 던지거나 아예 예외를 던지지 않을 수도 있습니다.

 

 

2) Putting It All Together

이전 섹션에서는 ListOfNumbers 클래스의 writeList 메서드를 위한 try, catch 및 finally 코드 블록을 구성하는 방법에 대해 설명했습니다. 이제 코드를 살펴보고 어떤 일이 발생할 수 있는지 조사해 봅시다.

모든 구성 요소를 합치면, writeList 메서드는 다음과 같이 보입니다.

public void writeList() {
    PrintWriter out = null;

    try {
        System.out.println("Entering" + " try statement");

        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " + list.get(i));
        }
    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           +  e.getMessage());
                                 
    } catch (IOException e) {
        System.err.println("Caught IOException: " +  e.getMessage());
                                 
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

 

앞서 언급했듯이, 이 메서드의 try 블록은 세 가지 다른 종료 가능성이 있습니다. 여기 그 중 두 가지가 있습니다.

1. try 스테이트먼트 내의 코드가 실패하여 예외를 던집니다. 이는 new FileWriter 문에서 발생한 IOException이거나 for 루프에서 잘못된 인덱스 값으로 인해 발생한 IndexOutOfBoundsException일 수 있습니다.
2. 모든 것이 성공하고 try 스테이트먼트가 정상적으로 종료됩니다.

이 두 가지 종료 가능성 동안 writeList 메서드에서 어떤 일이 발생하는지 살펴보겠습니다.

 

3) Scenario 1: An Exception Occurs

FileWriter를 생성하는 스테이트먼트는 여러 가지 이유로 실패할 수 있습니다. 예를 들어, 프로그램이 지정된 파일을 생성하거나 쓸 수 없는 경우 FileWriter 생성자는 IOException을 던집니다.

FileWriter가 IOException을 던지면 런타임 시스템은 즉시 try 블록 실행을 중지하고, 실행 중인 메서드 호출은 완료되지 않습니다. 그런 다음 런타임 시스템은 적절한 예외 핸들러를 찾기 위해 메서드 호출 스택의 맨 위에서 검색을 시작합니다. 이 예제에서 IOException이 발생하면 FileWriter 생성자가 호출 스택의 맨 위에 있습니다. 그러나 FileWriter 생성자에는 적절한 예외 핸들러가 없으므로 런타임 시스템은 메서드 호출 스택의 다음 메서드인 writeList 메서드를 확인합니다. writeList 메서드에는 IOException과 IndexOutOfBoundsException을 처리하는 두 개의 예외 핸들러가 있습니다.

런타임 시스템은 try 스테이트먼트 뒤에 나타나는 순서대로 writeList의 예외 핸들러를 확인합니다. 첫 번째 예외 핸들러의 아규먼트는 IndexOutOfBoundsException입니다. 이는 던져진 예외 타입과 일치하지 않으므로 런타임 시스템은 다음 예외 핸들러인 IOException을 확인합니다. 이는 던져진 예외 타입과 일치하므로 런타임 시스템은 적절한 예외 핸들러를 찾는 검색을 종료합니다. 런타임이 적절한 핸들러를 찾으면, 해당 catch 블록의 코드가 실행됩니다.

예외 핸들러가 실행된 후, 런타임 시스템은 제어를 finally 블록으로 전달합니다. finally 블록의 코드는 위에서 잡힌 예외와 상관없이 실행됩니다. 이 시나리오에서는 FileWriter가 열리지 않았기 때문에 닫을 필요가 없습니다. finally 블록 실행이 끝나면 프로그램은 finally 블록 이후의 첫 번째 스테이트먼트로 계속 실행됩니다.

다음은 IOException이 발생했을 때 나타나는 ListOfNumbers 프로그램의 전체 출력입니다.

Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open

 

다음 목록에서 굵게 표시된 코드는 이 시나리오에서 실행되는 문장들을 보여줍니다:

public void writeList() {
   PrintWriter out = null;  

    try {
        System.out.println("Entering try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++)
            out.println("Value at: " + i + " = " + list.get(i));
                               
    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           + e.getMessage());
                                 
    } catch (IOException e) {
        System.err.println("Caught IOException: " + e.getMessage());
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

 

4) Scenario 2: The try Block Exits Normally

이 시나리오에서는 try 블록 범위 내의 모든 스테이트먼트가 성공적으로 실행되고 예외를 던지지 않습니다. 실행은 try 블록의 끝으로 떨어지고, 런타임 시스템은 제어를 finally 블록으로 전달합니다. 모든 것이 성공했기 때문에, finally 블록에 도달했을 때 PrintWriter가 열려 있으므로, finally 블록에서 PrintWriter를 닫습니다. 마찬가지로, finally 블록 실행이 끝난 후 프로그램은 finally 블록 이후의 첫 번째 스테이트먼트로 계속 실행됩니다.

다음은 예외가 발생하지 않았을 때 ListOfNumbers 프로그램의 출력입니다.

Entering try statement
Closing PrintWriter

 

다음 예제에서 굵게 표시된 코드는 이 시나리오에서 실행되는 스테이트먼트들을 보여줍니다.

 

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entering try statement");
        out = new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++)
            out.println("Value at: " + i + " = " + list.get(i));
                  
    } catch (IndexOutOfBoundsException e) {
        System.err.println("Caught IndexOutOfBoundsException: "
                           + e.getMessage());

    } catch (IOException e) {
        System.err.println("Caught IOException: " + e.getMessage());
                                 
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } 
        else {
            System.out.println("PrintWriter not open");
        }
    }
}

 

 

5) Specifying the Exceptions Thrown by a Method

이전 섹션에서는 ListOfNumbers 클래스의 writeList 메서드를 위한 예외 핸들러를 작성하는 방법을 보여주었습니다. 때로는 코드 내에서 발생할 수 있는 예외를 잡는 것이 적절합니다. 그러나 다른 경우에는 호출 스택의 상위 메서드가 예외를 처리하도록 하는 것이 더 좋습니다. 예를 들어, ListOfNumbers 클래스를 클래스 패키지의 일부로 제공하는 경우, 패키지 사용자의 모든 요구를 예상할 수는 없습니다. 이 경우 예외를 잡지 않고 호출 스택 상위 메서드가 예외를 처리하도록 하는 것이 더 좋습니다.

writeList 메서드가 발생할 수 있는 체크 예외를 잡지 않는 경우, writeList 메서드는 이러한 예외를 던질 수 있음을 지정해야 합니다. 원래의 writeList 메서드를 수정하여 예외를 잡는 대신 던질 수 있는 예외를 지정해 봅시다. 원래 컴파일되지 않는 writeList 메서드는 다음과 같습니다.

public void writeList() {
    PrintWriter out = new PrintWriter(new FileWriter("OutFile.txt"));
    for (int i = 0; i < SIZE; i++) {
        out.println("Value at: " + i + " = " + list.get(i));
    }
    out.close();
}

 

writeList 메서드가 두 개의 예외를 던질 수 있음을 지정하려면, writeList 메서드 선언에 throws 절을 추가하십시오. throws 절은 throws 키워드 뒤에 쉼표로 구분된 해당 메서드가 던지는 모든 예외의 목록으로 구성됩니다. 이 절은 메서드 이름과 인수 목록 뒤, 메서드의 범위를 정의하는 중괄호 앞에 위치합니다. 다음은 예제입니다.

public void writeList() throws IOException, IndexOutOfBoundsException {

 

IndexOutOfBoundsException은 unchecked exception이므로 throws 절에 포함하는 것은 필수가 아닙니다. 다음과 같이 작성할 수 있습니다.

public void writeList() throws IOException {

 

 

6) How to Throw Exceptions

예외를 잡기 전에, 어떤 코드에서든 예외를 던져야 합니다. 모든 코드는 예외를 던질 수 있습니다: 여러분의 코드, 다른 사람이 작성한 패키지의 코드(예: 자바 플랫폼과 함께 제공되는 패키지), 또는 자바 런타임 환경의 코드가 예외를 던질 수 있습니다. 예외를 던지는 주체가 무엇이든, 예외는 항상 throw 스테이트먼트을 사용하여 던져집니다.

아마도 눈치채셨겠지만, 자바 플랫폼은 수많은 예외 클래스를 제공합니다. 모든 클래스는 Throwable 클래스의 자손이며, 모두 프로그램이 실행 중 발생할 수 있는 다양한 타입의 예외를 구분할 수 있게 해줍니다.

또한, 여러분이 작성한 클래스 내에서 발생할 수 있는 문제를 나타내기 위해 자체 예외 클래스를 만들 수도 있습니다. 사실, 패키지 개발자인 경우, 여러분의 패키지에서 발생할 수 있는 오류를 자바 플랫폼이나 다른 패키지에서 발생하는 오류와 구분할 수 있도록 자체 예외 클래스 집합을 만들어야 할 수도 있습니다.

chained exceptions도 만들 수 있습니다. 자세한 내용은 "Chained Exceptions" 섹션을 참조하십시오.

 

7) The throw Statement

모든 메서드는 throw 스테이트먼트를 사용하여 예외를 던집니다. throw 스테이트먼트는 하나의 아규먼트를 필요로 합니다: throwable 객체. throwable 객체는 Throwable 클래스의 하위 클래스의 인스턴스입니다. 다음은 throw 스테이트먼트의 예입니다.

throw someThrowableObject;

 

throw 스테이트먼트를 실제로 사용하는 예를 살펴봅시다. 다음 pop 메서드는 일반적인 스택 객체를 구현하는 클래스에서 가져온 것입니다. 이 메서드는 스택의 최상위 요소를 제거하고 해당 객체를 리턴합니다.

public Object pop() {
    Object obj;

    if (size == 0) {
        throw new EmptyStackException();
    }

    obj = objectAt(size - 1);
    setObjectAt(size - 1, null);
    size--;
    return obj;
}

 

pop 메서드는 스택에 엘리먼트가 있는지 확인합니다. 스택이 비어 있으면(크기가 0인 경우), pop 메서드는 새로운 EmptyStackException 객체(java.util의 멤버)를 인스턴스화하여 던집니다. 이 장의 "예외 클래스 생성" 섹션에서는 자체 예외 클래스를 생성하는 방법을 설명합니다. 지금은 java.lang.Throwable 클래스를 상속하는 객체만 던질 수 있다는 것만 기억하면 됩니다.

pop 메서드의 선언에는 throws 절이 포함되어 있지 않다는 점에 유의하십시오. EmptyStackException은 unchecked exception이므로, pop 메서드는 이를 발생시킬 수 있음을 명시할 필요가 없습니다.

 

8) Throwable Class and Its Subclasses

Throwable 클래스를 상속하는 객체에는 직접 자손[descendants](직접 Throwable 클래스를 상속하는 객체)과 간접 자손(자손 또는 손자 클래스에서 상속하는 객체)이 포함됩니다. 아래 그림은 Throwable 클래스와 그 주요 하위 클래스들의 클래스 계층 구조를 보여줍니다. 보시다시피, Throwable에는 두 개의 직접 자손이 있습니다: Error와 Exception.

The Throwable class.

 

 

 5. Error Class 

동적 연결[dynamic linking] 실패 또는 자바 가상 머신에서의 기타 심각한 실패가 발생하면 가상 머신은 Error를 던집니다. 일반적으로 간단한 프로그램은 Error를 catch 하거나 throw 하지 않습니다.

 

1) Exception Class

대부분의 프로그램은 Exception 클래스에서 파생된 객체를 던지고 잡습니다. Exception은 문제가 발생했음을 나타내지만 심각한 시스템 문제는 아닙니다. 대부분의 프로그램은 Error 대신 Exception을 throw 하고  catch 할 것입니다.

자바 플랫폼은 Exception 클래스의 많은 자손들을 정의합니다. 이러한 자손들은 발생할 수 있는 다양한 타입의 예외를 나타냅니다. 예를 들어, IllegalAccessException은 특정 메서드를 찾을 수 없음을 신호하며, NegativeArraySizeException은 프로그램이 음수 크기의 배열을 생성하려고 시도했음을 나타냅니다.

Exception의 한 하위 클래스인 RuntimeException은 API의 잘못된 사용을 나타내는 예외를 위해 예약되어 있습니다. 런타임 예외의 예로는 NullPointerException이 있으며, 이는 메서드가 null 참조를 통해 객체의 멤버에 접근하려고 할 때 발생합니다. "Unchecked Exceptions — The Controversy" 섹션에서는 대부분의 애플리케이션이 런타임 예외를 던지거나 RuntimeException을 서브클래스로 만들지 않아야 하는 이유를 논의합니다.

 

2) Chained Exceptions

애플리케이션은 종종 다른 예외를 발생시켜 특정 예외에 응답합니다. 실제로 첫 번째 예외로 인해 두 번째 예외가 발생합니다. 한 예외가 언제 또 다른 예외를 발생시키는지 아는 것은 매우 도움이 될 수 있습니다. chained exception는 프로그래머가 이를 수행하는 데 도움이 됩니다.

다음은 chained exception를 지원하는 Throwable 클래스의 메서드와 생성자입니다.

Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)

 

initCause 메서드와 Throwable 생성자의 Throwable 아규먼트는 현재 예외를 유발한 예외입니다. getCause 메서드는 현재 예외를 유발한 예외를 리턴하고, initCause 메서드는 현재 예외의 원인을 설정합니다.

 

public Throwable(String message, Throwable cause) {
    fillInStackTrace();
    detailMessage = message;
    this.cause = cause;
}

 

다음 예제는 chained exception를 사용하는 방법을 보여줍니다.

try {

} catch (IOException e) {
    throw new SampleException("Other IOException", e);
}

 

이 예제에서 IOException이 발생하면 원래의 원인을 포함한 새로운 SampleException 예외가 생성되고, 예외 체인이 상위 레벨의 예외 핸들러[자바 런타임에게 전달, 런타임은 호출 스택에서 적합한 핸들러를 찾아 전달]로 전달됩니다.

아래 코드는 Trowable의 initCause 메서드로 해당 예외를 촉발(생성)하게 한 원인 예외 클래스를 설정합니다.

public class InitCauseExample {
    public static void main(String[] args) {
        try {
            // 첫 번째 예외 발생
            throw new NumberFormatException("잘못된 숫자 형식입니다.");
        } catch (NumberFormatException e) {
            // 첫 번째 예외를 원인으로 두 번째 예외를 발생시킴
            IllegalArgumentException illegalArgumentException = new IllegalArgumentException("숫자 변환 중 오류가 발생했습니다.");
            illegalArgumentException.initCause(e);
            throw illegalArgumentException;
        }
    }
}

 

3) Accessing Stack Trace Information

이제 상위 레벨 예외 핸들러가 자체 포맷으로 스택 추적을 덤프하고 싶다고 가정해 봅시다.


정의: 스택 추적은 현재 스레드의 실행 이력에 대한 정보를 제공하며, 예외가 발생했을 때 호출된 클래스 및 메서드의 이름을 나열합니다. 스택 추적은 예외가 발생했을 때 주로 활용하는 유용한 디버깅 도구입니다.


다음 코드는 예외 객체에서 getStackTrace 메서드를 호출하는 방법을 보여줍니다.

catch (Exception cause) {
    StackTraceElement elements[] = cause.getStackTrace();
    for (int i = 0, n = elements.length; i < n; i++) {       
        System.err.println(elements[i].getFileName()
            + ":" + elements[i].getLineNumber() 
            + ">> "
            + elements[i].getMethodName() + "()");
    }
}

 

 6. Logging API 

다음 코드 스니펫은 catch 블록 내에서 예외가 발생한 위치를 기록합니다. 그러나 스택 추적을 수동으로 파싱하여 System.err()에 출력하는 대신, java.util.logging 패키지의 로깅 기능을 사용하여 출력을 파일로 보냅니다.

try {
    Handler handler = new FileHandler("OutFile.log");
    Logger.getLogger("").addHandler(handler);
    
} catch (IOException e) {
    Logger logger = Logger.getLogger("package.name"); 
    StackTraceElement elements[] = e.getStackTrace();
    for (int i = 0, n = elements.length; i < n; i++) {
        logger.log(Level.WARNING, elements[i].getMethodName());
    }
}

 


하따.. 오늘은 좀 뭐가 많네요..

복습만이 살길입니다.. 후아....

이상입니다!