예외 포착 및 처리
이 섹션에서는 세 가지 예외 처리기 구성 요소인 try, catch, finally 블록을 사용하여 예외 처리기를 작성하는 방법을 설명합니다. 그런 다음 Java SE 7에 도입된 try-with-resources 문에 대해 설명합니다. try-with-resources 문은 스트림과 같이 Closeable 리소스를 사용하는 상황에 특히 적합합니다.
이 섹션의 마지막 부분에서는 예제를 통해 다양한 시나리오에서 어떤 일이 발생하는지 분석합니다.
다음 예제에서는 ListOfNumbers라는 클래스를 정의하고 구현합니다. 생성되면 ListOfNumbers는 0부터 9까지의 순차적 값을 가진 10개의 Integer 요소를 포함하는 ArrayList를 생성합니다. ListOfNumbers 클래스는 또한 숫자 목록을 OutFile.txt라는 텍스트 파일에 기록하는 writeList()라는 메서드를 정의합니다. 이 예제에서는 기본 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<>(SIZE);
for (int i = 0; i < SIZE; i++) {
list.add(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();
}
}굵은 글씨로 표시된 첫 번째 줄은 생성자에 대한 호출입니다. 생성자는 파일의 출력 스트림을 초기화합니다. 파일을 열 수 없는 경우 생성자는 IOException을 던집니다. 굵은 글씨로 표시된 두 번째 줄은 인자 값이 너무 작거나(0보다 작음) 너무 크면(현재 ArrayList에 포함된 요소 수보다 많으면) IndexOutOfBoundsException을 throw하는 ArrayList 클래스의 get 메서드에 대한 호출입니다.
ListOfNumbers 클래스를 컴파일하려고 하면 컴파일러는 FileWriter 생성자가 던진 예외에 대한 오류 메시지를 출력합니다. 그러나 get()에서 발생하는 예외에 대한 오류 메시지는 표시하지 않습니다. 그 이유는 생성자에서 던지는 예외인 IOException은 체크된 예외이고, get() 메서드에서 던지는 예외인 IndexOutOfBoundsException은 체크되지 않은 예외이기 때문입니다.
이제 ListOfNumbers 클래스와 그 안에서 예외를 던질 수 있는 위치에 익숙해졌으므로 예외를 포착하고 처리하는 예외 핸들러를 작성할 준비가 되었습니다.
Try 블록
예외 처리기를 구성하는 첫 번째 단계는 예외를 발생시킬 수 있는 코드를 try 블록 안에 묶는 것입니다. 일반적으로 try 블록은 다음과 같이 생깁니다:
try {
code
}
catch and finally blocks . . .레이블이 지정된 코드 예시의 세그먼트에는 예외를 발생시킬 수 있는 하나 이상의 유효한 코드 줄이 포함되어 있습니다. (catch 및 finally 블록은 다음 두 하위 섹션에서 설명합니다.)
ListOfNumbers 클래스의 writeList() 메서드에 대한 예외 처리기를 구성하려면 try 블록 안에 writeList() 메서드의 예외를 던지는 문을 묶습니다. 이 작업을 수행하는 방법은 여러 가지가 있습니다. 예외를 발생시킬 수 있는 각 코드 줄을 자체 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");
out = new PrintWriter(new FileWriter("OutFile.txt"));
for (int i = 0; i < SIZE; i++) {
out.println("Value at: " + i + " = " + list.get(i));
}
}
catch and finally blocks . . .
}try 블록 내에서 예외가 발생하면 해당 예외는 이와 연결된 예외 처리기에 의해 처리됩니다. 예외 처리기를 try 블록에 연결하려면 그 뒤에 catch 블록을 넣어야 합니다. 다음 섹션인 캐치 블록에서 그 방법을 설명합니다.
Catch 블록
try 블록 바로 뒤에 하나 이상의 catch 블록을 제공함으로써 예외 처리기를 try 블록과 연결합니다. try 블록의 끝과 첫 번째 catch 블록의 시작 사이에 어떤 코드도 있을 수 없습니다.
try {
} catch (ExceptionType name) {
} catch (ExceptionType name) {
}각 catch 블록은 인자로 표시된 예외 타입을 처리하는 예외 핸들러입니다. 인자 타입인 ExceptionType은 핸들러가 처리할 수 있는 예외의 타입을 선언하며, Throwable 클래스로부터 상속된 클래스의 이름이어야 합니다. 핸들러는 이름으로 예외를 참조할 수 있습니다.
catch 블록에는 예외 처리기가 호출될 때 실행되는 코드가 포함되어 있습니다. 런타임 시스템은 핸들러가 호출 스택에서 ExceptionType이 던져진 예외의 타입과 일치하는 첫 번째 핸들러일 때 예외 핸들러를 호출합니다. 시스템은 던져진 객체를 예외 처리기의 인수에 합법적으로 할당할 수 있는 경우 일치하는 것으로 간주합니다.
다음은 writeList() 메서드에 대한 두 가지 예외 처리기입니다:
try {
} catch (IndexOutOfBoundsException e) {
System.err.println("IndexOutOfBoundsException: " + e.getMessage());
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
}예외 처리기는 단순히 오류 메시지를 출력하거나 프로그램을 중지하는 것 이상의 작업을 수행할 수 있습니다. 연쇄 예외 섹션에 설명된 대로 연쇄 예외를 사용하여 오류를 복구하고, 사용자에게 결정을 내리도록 메시지를 표시하거나, 상위 수준 처리기로 오류를 전파할 수 있습니다.
Multi-Catching Exceptions
멀티캐치 패턴을 사용하면 하나의 예외 처리기로 두 가지 이상의 예외 유형을 잡을 수 있습니다.
Java SE 7 이상에서는 하나의 catch 블록이 두 가지 이상의 예외 유형을 처리할 수 있습니다. 이 기능을 사용하면 코드 중복을 줄이고 지나치게 광범위한 예외를 잡으려는 유혹을 줄일 수 있습니다.
catch 절에서 블록이 처리할 수 있는 예외 유형을 지정하고 각 예외 유형을 세로 막대(|)로 구분하세요:
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}Note: catch 블록이 두 개 이상의 예외 타입을 처리하는 경우 catch 매개변수는 암시적으로 final입니다. 이 예제에서 catch 매개변수 ex는 final이므로 catch 블록 내에서 어떤 값도 할당할 수 없습니다.
Finally 블록
finally 블록은 try 블록이 종료될 때 항상 실행됩니다. 이렇게 하면 예기치 않은 예외가 발생하더라도 finally 블록이 실행됩니다. 그러나 finally는 단순한 예외 처리 외에도 프로그래머가 실수로 return, continue 또는 break에 의해 정리 코드가 우회되는 것을 방지하는 데 유용합니다. 예외가 예상되지 않는 경우에도 finally 블록에 정리 코드를 넣는 것은 항상 좋은 습관입니다.
Note:
try또는catch코드가 실행되는 동안 JVM이 종료되면finally블록이 실행되지 않을 수 있습니다.
여기서 작업하고 있는 writeList() 메서드의 try 블록은 PrintWriter를 엽니다. 프로그램은 writeList() 메서드를 종료하기 전에 해당 스트림을 닫아야 합니다. 이는 다소 복잡한 문제를 야기하는데, writeList()의 try 블록이 다음 세 가지 방법 중 하나로 종료될 수 있기 때문입니다.
- new
FileWriter문이 실패하고IOException을 던집니다. list.get(i)문이 실패하고IndexOutOfBoundsException을 던집니다.- 모든 것이 성공하고 ‘try’ 블록이 정상적으로 종료됩니다.
런타임 시스템은 ‘try’ 블록 내에서 어떤 일이 발생하든 항상 ‘finally’ 블록 내의 문을 실행합니다. 따라서 정리를 수행하기에 완벽한 장소입니다.
마지막으로 writeList() 메서드에 대한 다음 finally 블록은 PrintWriter를 정리한 다음 닫습니다.
finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
} else {
System.out.println("PrintWriter not open");
}
}중요:
finally블록은 리소스 유출을 방지하는 핵심 도구입니다. 파일을 닫거나 다른 방법으로 리소스를 복구할 때는finally블록에 코드를 배치하여 리소스가 항상 복구되도록 하세요.이러한 상황에서는 더 이상 필요하지 않을 때 시스템 리소스를 자동으로 해제하는 try-with-resources 문을 사용하는 것을 고려하세요. 자세한 내용은 try-with-resources 문 섹션을 참조하세요.
try-with-resources 문
try-with-resources 문은 하나 이상의 리소스를 선언하는 try 문입니다. 리소스는 프로그램이 완료된 후 닫아야 하는 객체입니다. try-with-resources 문은 문이 끝날 때 각 리소스가 닫히도록 합니다. java.io.Closeable을 구현하는 모든 객체를 포함하여 java.lang.AutoCloseable을 구현하는 모든 객체를 리소스로 사용할 수 있습니다.
다음 예제는 파일에서 첫 줄을 읽습니다. 이 예제는 BufferedReader의 인스턴스를 사용하여 파일에서 데이터를 읽습니다. BufferedReader는 프로그램이 완료된 후 닫아야 하는 리소스입니다:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}이 예제에서 try-with-resources 문에 선언된 리소스는 BufferedReader입니다. 선언문은 try 키워드 바로 뒤에 괄호 안에 나타납니다. Java SE 7 이상에서 BufferedReader 클래스는 java.lang.AutoCloseable 인터페이스를 구현합니다. BufferedReader 인스턴스는 try-with-resource 문에서 선언되므로 try 문이 정상적으로 완료되었는지 또는 갑작스럽게 완료되었는지(메서드 BufferedReader.readLine()가 IOException을 throw하는 결과로 인해)에 관계없이 닫힙니다.
Java SE 7 이전 버전에서는 finally 블록을 사용하여 try 문이 정상적으로 완료되는지 또는 갑자기 완료되는지 여부에 관계없이 리소스가 닫히도록 할 수 있습니다. 다음 예제에서는 try-with-resources 문 대신 finally 블록을 사용합니다:
static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}그러나 이 예제에서 readLine() 및 close 메서드가 모두 예외를 던지면 readFirstLineFromFileWithFinallyBlock() 메서드는 finally 블록에서 던진 예외를 던지고, try 블록에서 던진 예외는 억제됩니다. 반대로, 예제 readFirstLineFromFile()에서 try 블록과 try-with-resources 문 모두에서 예외가 발생하면 readFirstLineFromFile() 메서드는 try 블록에서 발생한 예외를 던지고 try-with-resources 블록에서 발생한 예외는 억제됩니다. Java SE 7 이상에서는 억제된 예외를 검색할 수 있습니다. 자세한 내용은 억제된 예외 섹션을 참조하십시오.
try-with-resources 문에서 하나 이상의 리소스를 선언할 수 있습니다. 다음 예제는 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() 메서드가 이 순서대로 자동으로 호출됩니다. 리소스의 닫기 메서드는 생성 순서의 반대 순서로 호출된다는 점에 유의하세요.
다음 예는 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의 일부입니다.
Note: try-with-resources 문에는 일반
try문과 마찬가지로catch및finally블록이 있을 수 있습니다. try-with-resources 문에서는 선언된 리소스가 닫힌 후에catch또는finally블록이 실행됩니다.
억제된 예외
try-with-resources 문과 연결된 코드 블록에서 예외가 발생할 수 있습니다. 예제 writeToFileZipFileContents()에서 try 블록에서 예외가 발생할 수 있으며, try-with-resources 문에서 ZipFile 및 BufferedWriter 객체를 닫으려고 할 때 최대 두 개의 예외가 발생할 수 있습니다. try 블록에서 예외가 발생하고 try-with-resources 문에서 하나 이상의 예외가 발생하면 try-with-resources 문에서 발생한 예외는 억제되며, 이 블록에서 발생한 예외는 writeToFileZipFileContents() 메서드에서 발생한 예외가 됩니다. try 블록에서 던진 예외에서 Throwable.getSuppressed() 메서드를 호출하여 이러한 억제된 예외를 검색할 수 있습니다.
AutoCloseable 또는 Closeable 인터페이스를 구현하는 클래스
이 인터페이스 중 하나를 구현하는 클래스 목록은 AutoCloseable 및 Closeable 인터페이스의 자바독을 참조하세요. Closeable 인터페이스는 AutoCloseable 인터페이스를 확장한 것입니다. Closeable 인터페이스의 close() 메서드는 IOException 타입의 예외를 던지고, AutoCloseable 인터페이스의 close() 메서드는 Exception 타입의 예외를 던집니다. 따라서 AutoCloseable 인터페이스의 서브클래스는 close() 메서드의 이 동작을 재정의하여 IOException과 같은 특수 예외를 던지거나 아예 예외를 던지지 않을 수 있습니다.
모든 것 종합하기
이전 섹션에서는 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’ 블록에는 세 가지 종료 가능성이 있으며, 그 중 두 가지가 있습니다.
try문에 있는 코드가 실패하고 예외가 발생합니다. 새로운FileWriter문으로 인해 발생하는IOException또는for루프에서 잘못된 인덱스 값으로 인해 발생하는IndexOutOfBoundsException이 될 수 있습니다.- 모든 것이 성공하고
try문이 정상적으로 종료됩니다.
이 두 가지 종료 가능성 중 writeList() 메서드에서 어떤 일이 발생하는지 살펴봅시다.
Scenario 1: 예외 발생
FileWriter를 생성하는 문은 여러 가지 이유로 실패할 수 있습니다. 예를 들어, 프로그램이 지정된 파일을 만들거나 해당 파일에 쓸 수 없는 경우 FileWriter의 생성자가 IOException을 던집니다.
FileWriter가 IOException을 던지면 런타임 시스템은 즉시 try 블록의 실행을 중지하고 실행 중인 메서드 호출은 완료되지 않습니다. 그런 다음 런타임 시스템은 메서드 호출 스택의 최상위에서 적절한 예외 처리기를 검색하기 시작합니다. 이 예제에서는 IOException이 발생하면 FileWriter 생성자가 호출 스택의 맨 위에 있습니다. 그러나 FileWriter 생성자에는 적절한 예외 처리기가 없으므로 런타임 시스템은 메서드 호출 스택에서 다음 메서드인 writeList() 메서드를 확인합니다. writeList() 메서드에는 IOException과 IndexOutOfBoundsException에 대한 두 가지 예외 처리기가 있습니다.
런타임 시스템은 writeList()의 핸들러를 try 문 뒤에 나타나는 순서대로 확인합니다. 첫 번째 예외 핸들러의 인수는 IndexOutOfBoundsException입니다. 이 예외의 타입과 일치하지 않으므로 런타임 시스템은 다음 예외 처리기인 IOException을 확인합니다. 이 예외 처리기가 던져진 예외의 타입과 일치하므로 런타임 시스템은 적절한 예외 처리기에 대한 검색을 종료합니다. 이제 런타임이 적절한 핸들러를 찾았으므로 해당 catch 블록의 코드가 실행됩니다.
예외 처리기가 실행된 후 런타임 시스템은 finally 블록으로 제어권을 넘깁니다. finally 블록의 코드는 그 위에 잡힌 예외와 상관없이 실행됩니다. 이 시나리오에서는 FileWriter가 열리지 않았으므로 닫을 필요가 없습니다. finally 블록의 실행이 끝나면 프로그램은 finally 블록 뒤의 첫 번째 문으로 계속 진행됩니다.
다음은 IOException이 발생했을 때 나타나는 ListOfNumbers 프로그램의 전체 출력입니다.
Entering try statement
Caught IOException: OutFile.txt
PrintWriter not openScenario 2: try 블록이 정상적으로 종료됩니다.
이 시나리오에서는 try 블록 범위 내의 모든 문이 성공적으로 실행되고 예외가 발생하지 않습니다. 실행은 try 블록의 끝에서 떨어지고 런타임 시스템은 finally 블록으로 제어권을 넘깁니다. 모든 것이 성공했기 때문에 제어권이 finally 블록에 도달하면 PrintWriter가 열려 있고, 이 블록은 PrintWriter를 닫습니다. 다시 말하지만, finally 블록의 실행이 끝나면 프로그램은 finally 블록 뒤의 첫 번째 문으로 계속됩니다.
다음은 예외가 발생하지 않았을 때 ListOfNumbers 프로그램의 출력입니다.
Entering try statement
Closing PrintWriter