자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야 하는 자원이 많다.
InputStream, OutputStream, java.sql.Connection 등이 좋은 예다.
자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다.
이런 자원 중 상당수가 안전망으로 finalizer를 활용하고는 있지만 finalizer는 그리 믿을만 하지 못하다.
전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰였다.
// 코드 9-1 try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! (47쪽)
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
자원을 중첩적으로 사용하는 경우에는 너무 지저분하다.
// 코드 9-2 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! (47쪽)
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();
}
}
2007년 자바 라이브러리에서 close 메서드를 제대로 구현한 비율은 겨우 1/3 정도다.
try-finally 문을 제대로 사용한 앞의 두 코드 예제에 조차 미묘한 결점이 있다.
예외는 try 블록과 finally 블록 모두에서 발생할 수 있는데, 예컨대 기기에 물리적인 문제가 생긴다면 firstLineOfFile 메서드 안의 readLine 메서드가 예외를 던지고 같은 이유로 close 메서드도 실패할 것이다.
이런 상황이라면 두번째 예외가 첫 번째 예외를 완전히 집어삼켜 버린다.
그러면 스택 추적 내역에 첫 번째 예외에 관한 정보는 남지 않게 되어 실제 시스템에서의 디버깅을 몹시 어렵게 한다.
물론 두 번째 예외 대신 첫 번째 예외를 기록하도록 코드를 수정할 수는 있지만 코드가 너무 지저분해져서 실제로 그렇게까지 하는 경우는 거의 없다.
이러한 문제들은 자바 7의 try-with-resources 덕에 모두 해결되었다.
이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 한다.
자바 라이브러리와 서드파티 라이브러리들의 수많은 클래스와 인터페이스가 이미 AutoCloseable을 구현하거나 확장해뒀다.
우리도 닫아야하는 자원을 뜻하는 클래스를 작성한다면 AutoCloseable을 반드시 구현하자.
아래는 try-with-resources를 사용해 맨 처음의 코드를 재작성한 예이다.
// 코드 9-3 try-with-resources - 자원을 회수하는 최선책! (48쪽)
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}
두번째 코드를 재작성 한 예이다.
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);
}
}
짧고 읽기 수월할 뿐 아니라 문제를 진단하기도 훨씬 좋다.
firstLineOfFile 메서드를 생각해보면 readLine과 close 호출 양쪽에서 예외가 발생하면 close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다.
보통의 try-finally에서 처럼 try-with-resources에서도 catch 절을 쓸 수 있다.
catch 절 덕분에 try 문을 중첩하지 않고도 다수의 예외를 처리할 수 있다.
다음 코드에서는 firstLineOfFile 메서드를 살짝 수정하여 파일을 열거나 데이터를 읽지 못했을 때 예외를 던지는 대신 기본값을 반환하는 코드이다.
예가 살짝 어색하지만 저런식으로 구현할 수 있다.
// 코드 9-4 복수의 자원을 처리하는 try-with-resources - 짧고 매혹적이다! (49쪽)
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);
}
}
꼭 해수해야 하는 자원을 다룰 때는 try-finally 말고 try-with-resources를 사용하자
예외는 없다. 코드는 더 짧고 분명해지고 만들어지는 예외 정보도 훨씬 유용하다.
try-finally로 작성하면 실용적이지 못할 만큼 코드가 지저분해지는 경우라도 try-with-resources로는 정확하고 쉽게 자원을 회수할 수 있다.
'Book > Effective Java' 카테고리의 다른 글
[Effective Java]11.equals를 재정의하면 hashCode도 재정의하라 (0) | 2022.06.04 |
---|---|
[Effective Java]10.equals는 일반 규약을 지켜 재정의하라 (0) | 2022.06.04 |
[Effective Java] 8.finalizer와 cleaner 사용을 피하라 (0) | 2022.06.03 |
[Effective Java] 7.다 쓴 객체 참조를 해제하라 (0) | 2022.06.03 |
[Effective Java] 6.불필요한 객체 생성을 피하라 (0) | 2022.06.03 |