java/java

[Java] Suppressed Exception

danuri 2024. 5. 6. 14:43

✅ Suppressed Exception

Suppressed Exception은 throw되지만 무시되는 예외를 말한다.

예를 들어, try-catch-finally 문에서 try문에서 예외가 발생했을 때, catch문에서 예외를 받아 throw한 상황에서,

finally 문에서도 예외가 발생했다면, try문에서 발생한 예외는 무시(suppressed)된다.

'무시'된 예외는 실제로 throw되지 않아, stacktrace에 찍히지 않는다.

-> 잘 이해가 안될 수 있는데, 다음 예제를 통해 더 자세하게 알아보자.

 

✅ try-catch-finally문에서 Suppressed Exception

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();
    }
}

만약 존재하는 파일에 대한 filaPath라면, 예외 없이 정상적으로 코드가 수행되겠지만,

존재하지 않는 파일에 대한 filaPath라면 어떻게 될까?

  1. try block에서 FileNotFoundException이 발생한다.
  2. catch block에서 IOException이 발생한다.
  3. finally block에서 NullPointerException이 발생한다. (fileIn이 초기화되지 않았기 때문)

 

이처럼 여러 Exception이 발생하는데, 실제로 코드를 수행해보면 NullPointerException만 throw되고, 실제 근본적인 문제인 "파일이 존재하지 않는다"를 알 수 있는 FileNotFoundException은 throw되지 않는다. (Suppressed)

Exception in thread "main" java.lang.NullPointerException
	at chapter12.SuppressedExceptionTest.demoSuppressedException(SuppressedExceptionTest.java:16)
	at chapter12.SuppressedExceptionTest.main(SuppressedExceptionTest.java:21)

 

테스트를 수행하면 다음과 같다.

@Test(expected = NullPointerException.class)
public void givenNonExistentFileName_whenAttemptFileOpen_thenNullPointerException() throws IOException {
    demoSuppressedException("/non-existent-path/non-existent-file.txt");
}

 

 

그렇다면, Suppressed된 Exception들도 모두 throw시키고 싶다면 어떻게 해야 할까?

바로 Throwable.addSuppressed()를 사용하는 방법이 있다.

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;
        }
    }
}

addSuppressed()를 통해 처음 발생했던 Exception을 추가한다.

 

이제 stacktrace를 통해 근본적인 원인을 발견하고 디버깅을 더 수월하게 할 수 있겠다.

Exception in thread "main" java.lang.NullPointerException
	at chapter12.SuppressedExceptionTest.demoAddSuppressedException(SuppressedExceptionTest.java:29)
	at chapter12.SuppressedExceptionTest.main(SuppressedExceptionTest.java:40)
	Suppressed: java.io.FileNotFoundException: /non-existent-path/non-existent-file.txt (No such file or directory)
		at java.base/java.io.FileInputStream.open0(Native Method)
		at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
		at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
		at java.base/java.io.FileInputStream.<init>(FileInputStream.java:112)
		at chapter12.SuppressedExceptionTest.demoAddSuppressedException(SuppressedExceptionTest.java:24)
		... 1 more

 

 

테스트를 수행하면 다음과 같다.

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));
}

 

그런데 생각해보면 파일을 사용하기 위해 try-catch-finally문을 수행할 때마다 저런 복잡한 코드들을 수행해줘야 하나 싶다.

이럴 때 사용하기 좋은게 try-with-resources문이다.

 

✅ try-with-resources문에서 Suppressed Exception

try-with-resources문은 Java 7부터 추가됐으며,

별도 finally문에서 resource를 close하지 않아도, 알아서 resource를 닫아주는 편리한 인터페이스를 제공한다.

 

먼저 try-with-resource문에 사용하기 위한 리소스 객체를 만들어준다.

public class ExceptionalResource implements AutoCloseable {

    public void processSomething() {
        throw new IllegalArgumentException("Thrown from processSomething()");
    }

    @Override
    public void close() throws Exception {
        throw new NullPointerException("Thrown from close()");
    }
}

try-wth-resource문은 AutoCloseable 인터페이스를 구현한 객체만 자동으로 close()를 호출해준다.

(앞서 사용했던 FileInputStream도 상속 구조를 올라가다 보면 AutoCloseable를 구현한 것을 볼 수 있다)

 

public static void demoExceptionalResource() throws Exception {
    try (ExceptionalResource exceptionalResource = new ExceptionalResource()) {
        exceptionalResource.processSomething();
    }
}

 

stacktrace를 찍어보면 별도로 addSuppressed()를 하지 않았는데도 Suppressed Exception까지 출력되는 것을 볼 수 있다.

Exception in thread "main" java.lang.IllegalArgumentException: Thrown from processSomething()
	at chapter12.ExceptionalResource.processSomething(ExceptionalResource.java:6)
	at chapter12.SuppressedExceptionTest.demoExceptionalResource(SuppressedExceptionTest.java:41)
	at chapter12.SuppressedExceptionTest.main(SuppressedExceptionTest.java:46)
	Suppressed: java.lang.NullPointerException: Thrown from close()
		at chapter12.ExceptionalResource.close(ExceptionalResource.java:11)
		at chapter12.SuppressedExceptionTest.demoExceptionalResource(SuppressedExceptionTest.java:40)
		... 1 more

 

테스트를 수행하면 다음과 같다.

try {
    demoExceptionalResource();
} catch (Exception e) {
    assertThat(e, instanceOf(IllegalArgumentException.class));
    assertEquals("Thrown from processSomething()", e.getMessage());
    assertEquals(1, e.getSuppressed().length);
    assertThat(e.getSuppressed()[0], instanceOf(NullPointerException.class));
    assertEquals("Thrown from close()", e.getSuppressed()[0].getMessage());
}

 

중요!

try-with-resources가 try-catch-finally와 다른 점이 있다.

try-catch-finally는 try block의 Exception이 Suppressed되는 반면,

try-with-resources는 마지막 close()할 때의 Exception이 Suppressed된다.

-> 근본적인 원인인 processSomething()에서 발생한 예외에 더 집중한다는 느낌이 있다.

-> 결론: 코드 가독성, Suppressed Exception 디버깅 등의 측면에서 웬만하면 try-with-resources를 사용하자.

 


 

참고자료

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