✅ 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라면 어떻게 될까?
- try block에서 FileNotFoundException이 발생한다.
- catch block에서 IOException이 발생한다.
- 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
'java > java' 카테고리의 다른 글
[Java] Comparator와 @FunctionalInterface (0) | 2024.03.03 |
---|---|
[Java] public class (0) | 2024.02.23 |
Mockito (0) | 2023.07.12 |
[Java] 등가속도 운동 - t초 후의 위치 계산 (0) | 2023.01.11 |
[Java] 메서드 애노테이션 정보 가져오기 (3) | 2022.03.12 |