Dev/Java

Try with Resource & Suppressed Exception

두넌 2023. 11. 13.

Try with Resource


어떤 언어에서든, 자원의 사용 후 반환은 매우 중요한 일이다

자원을 효율적으로 사용하기 위해서는, 우리는 사용 후 반드시 자원을 반환할 필요가 있다

 

일반적으로 우리는, 자원을 사용하고 반환하는 코드를 다음과 같이 try-catch를 위와 같은 형식으로 작성해왔을 것이다

    Socket socket = null;
    try {
        socket = new Socket("localhost", port);
        System.out.println("Port " + port + "is open");
        socket.close();
    } catch(IOException e) {
    // do something
    }

하지만 위의 경우에 만약 Socket Instance를 생성하는 도중에, Exception이 발생하게 되면

socket.close()는 정상적으로 이루어지지 않는다

 

따라서 

Socket socket = null;
try {
    socket = new Socket("localhost", port);
    System.out.println("Port " + port + "is open");
} catch (IOException e) {
// do something
} finally {
    socket.close();
}

다음과 같이 socket.close() 를 finally문에 작성하였지만, 소켓을 닫아주는 동작 자체에서도 IOException이 발생할 수 있으며 checked exception이므로 반드시 예외처리가 필요하다.

위 코드를 다음과 같이 Exception Handling한 아래와 같은 코드로 수정한다

Socket socket = null;
try {
    socket = new Socket("localhost", port);
    System.out.println("Port " + port + "is open");
} catch (IOException e) {
    // do something
} finally {
    try {
        socket.close();
    } catch (IOException e) {
        // do something
    }
}

이렇게 하면, Socket Instance를 생성하는 도중에 Exception이 발생하더라도 socket이 정상적으로 닫힐 수 있는 여지를 준다

하지만 이렇게 Resource를 사용하고 close하는 과정에서 불필요한 작업을 반복해야 하는 단점이 있다

 

이를 해결하고자, Try-with-resource 문법이 새로 추가되었다

 

Java7에서 소개되었으며, try 블럭 안에서 resource를 declare하고 해당 블록의 실행이 끝나면 Java가 알아서 Resource를 close해준다

만일 정상적으로 실행이 완료되거나, 중간에 Exception이 발생하더라도 close는 반드시 이루어지게 해주는 것이다

 

Try-with-Resource를 사용하려면, 여기서 말하는 Resource declare는 반드시 AutoCloseable interface를 Implement해야만 한다

https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html

AutoCloseable을 Implement한 잘 알려진 클래스들이다

위에서 예제로 사용했던 Socket도 보이고, BufferedReader등 많은 클래스들이 이를 Implement한다

 

Usage

try (Socket socket = new Socket("localhost", 1234)) {
    System.out.println("port " + 1234 + "is open");
}

위와 같이, try-with-resource 문의 괄호 안에 해당 객체를 생성하면, 사용자가 close()를 호출해주지 않아도 해당 블럭을 벗어나면 자동으로 호출된다

 

try (Socket socket = new Socket("localhost", 1234)) {
  System.out.println("port " + 1234 + "is open");
}catch (IOException e) {
  // do something
}

다음과 같이 Socket 객체를 생성할 때의 IOException을 Handling해주기만 하면, close()는 알아서 이 문장이 끝나면 호출될 것이다

또한 이 괄호 안에는 아래 예시처럼 여러 문장을 작성하는 것 또한 가능하다

 

// https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html
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());
        }
    }

위와 같이 두 문장 이상을 작성할 때에는, 세미콜론으로 문장을 구분해 주어야 한다

 

Suppressed Exception


Suppressed Exception에 관련된 예제는 모두 Baeldung을 참조하였음을 알려드립니다
https://www.baeldung.com/java-suppressed-exceptions
public static void demoSuppressedException(String filePath) throws IOException {
    FileInputStream fileIn = null;
    try {
        fileIn = new FileInputStream(filePath);
    } catch (FileNotFoundException e) {
        throw new IOException(e);
    } finally {
        fileIn.close();
    }
}

다음과 같은 특정 파일의 InputStream 객체를 만드는 예제가 있다고 하자,

특정 파일이 존재하는 파일이라면 상관없이 정상적으로 실행되겠지만, 만약 존재하지 않는 파일이라면 어떻게 될까?

 

  public static void main(String[] args) throws IOException {
    FileInputStream fileIn = null;
    try {
      fileIn = new FileInputStream("/non-existent");
    } catch (FileNotFoundException e) {
      throw new IOException(e);
    } finally {
      fileIn.close();
    }
  }

다음과 같이 존재하지 않는 경로를 임의로 입력하고, 프로그램을 실행시켜 보았더니

Exception in thread "main" java.lang.NullPointerException
	at solving.Main.main(Main.java:16)

다음과 같은 Exception이 fileIn.close() 과정에서 발생하였는데

존재하지 않는 경로이기 때문에 FileNotFoundException이 먼저 발생했을 것이며 catch 블럭으로 이동하여 throw new IOException(e) 코드가 실행되어야 맞지만

finally 블럭은 반드시 실행되어야 하기에, catch 블럭의 throw 전까지의 코드들이 실행된 후 finally 블럭으로 이동하여 fileIn.close() 를 실행하던 도중, NullPointerException이 발생한 것이다

 

문제점은, 이렇게 되면 기존에 던지려고(throw) 했던 근본적인 오류인 파일이 존재하지 않는 것..(FileNotFoundException)에 대한 어떠한 정보도 얻을 수 없게 되어버리는 것이다.

 

간단히 말하여, finally block에서 Exception을 throw한다면 기존 try block에서 throw된 Exception은 모두 Suppressed(무시) 된다는 것이다

 

이를 해결하기 위하여 Adding Suppressed Exception을 사용한다

public static void demoAddSuppressedException(String filePath) throws IOException {
    Throwable firstException = null;
    FileInputStream fileIn = null;
    try {
        fileIn = new FileInputStream(filePath);
    } catch (IOException e) {
        firstException = e;
    } finally {
        try {
            fileIn.close();
        } catch (NullPointerException npe) {
            if (firstException != null) {
                npe.addSuppressed(firstException);
            }
            throw npe;
        }
    }
}

다음과 같이, fileIn.close()를 감싸는 try-catch 블럭을 구성하고 NullPointerException을 catch하며

첫번째 Exception에 대한 정보를 미리 저장해 두고 이를 addSuppressed() 를 통하여 finally block에서 발생한 Exception에 추가해 준 후 throw 하면,

 

try {
    demoAddSuppressedException("/non-existent-path/non-existent-file.txt");
} catch (Exception e) {
    assertThat(e, instanceOf(NullPointerException.class));
    assertEquals(1, e.getSuppressed().length);
    assertThat(e.getSuppressed()[0], instanceOf(FileNotFoundException.class));
}

다음과 같은 상황에서 e 는 NullPointerException이지만,

e.getSuppressed() 은 해당 Exception에 Adding된 Suppresed Exception 리스트를 반환(Throwable[])하고,

(위 예시에서 length는 1일 것이다)

e.getSuppresed()[0] 은 기존 try block에서 발생했지만 무시되었던 Exception인, addSuppressed() 해주었던 FileNotFoundException임을 알 수 있다

 

이를 try-with-resource로 작성하면

    try (FileInputStream fileIn = new FileInputStream(filePath)) {
      //do something
    } catch (FileNotFoundException npe) {
      throw new IOException();
    }

위에서 언급된 try-with-resource를 사용하여 이를 작성한다면,

위 상황처럼 파일을 여는 도중 Exception을 throw하며 close() 에서 Exception이 Throw되더라도 알아서 close()에서 발생한 Suppressed된 Exception을 Throw해 주므로 위처럼 AddSuppressed() 를 사용하여 처리해줄 필요가 없을 것이다

 

 

Reference


https://www.baeldung.com/java-suppressed-exceptions

https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

댓글