자바 라이브러리에는 close 메서드를 호출해 직접 닫아주어야 하는 비메모리 자원들이 많다.
예를 들어, InputStream, OutputStream, java.sql.Connection 등이 좋은 예시다.
문제는 이 자원 닫기를 클라이언트 측에서 수행해주면 다행이지만, 놓치면 예측 불가능한 성능 문제로 이어진다.
전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 사용되었다.
📌 try-finally
public static String firstLineOfFile(String path) throw IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
문제는 다음과 같이 자원이 둘 이상일 때, try-finally 방식은 필요 이상으로 코드가 지저분해진다.
static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dst);
try {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
}
} finally {
in.close();
}
}
이 두 예제의 결점은 예외가 try, finally 블록 모두에서 발생할 수 있다.
readLine 메서드와, 자원을 해제하는 close 메서드 호출 동시에 예외가 발생하면, 두 번째 예외가 첫 번째 예외를 집어삼킨다.
그러면 스택 추적 내역에 첫 번째 예외 정보는 남지 않고, 디버깅을 굉장히 어렵게 만든다.
물론 두 번째 예외 대신 첫 번째 예외를 기록하도록 코드를 수정하는 것이 불가능하지 않지만 가독성이 바닥까지 떨어진다.
📌 try-with-resources
이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현할 필요는 있지만, AutoCloseable은 단순히 void를 반환하는 close 메서드 하나만 덩그러니 있다.
닫아야 하는 자원을 뜻하는 클래스를 작성한다면, 반드시 구현하자.
// 자원을 회수하는 최선책
public static String firstLineOfFile(String path) throw IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (Exception e) {
return defaultVal;
}
}
try-finally에서는 두 개 이상의 자원을 쓸 때, 중첩 try-finally가 발생해 코드가 지저분했었다.
하지만 복수의 자원을 처리하는 try-with-resources는 매우 짧고 직관적이다.
static void copy(String src, String dst) throws IOException {
try (InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dst)) {
byte[] buf = new byte[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
}
}
코드를 이렇게 작성하면 close가 알아서 호출해주며, 예외가 발생해도 close에서 발생한 예외는 숨겨지고 첫 번째 예외가 기록된다.
혹여 숨겨진 예외들은 스택 추적 내역에 suppressed 꼬리표를 달고 출력된다.
코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용한 try-with-resources를 사용해 자원을 회수하자.