본문은 Effective Java를 읽고 정리한 내용을 기반으로 작성된 글입니다.
[ item 8. finalizer와 cleaner 사용을 피하라 ]
자바는 두 가지 객체 소멸자를 제공한다.
☑️ finalizer
: 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
☑️ cleaner
: finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
- finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
- 얼마나 신속하게 수행될지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 동작 또한 마찬가지
- finalizer를 달아두면 그 인스턴스의 자원 회수가 제멋대로 지연될 수 있다.
- 수행 여부조차 보장하지 못한다.
- 따라서 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해선 안 된다.
- 심각한 성능 문제도 동반한다.
- try-with-resources 구문을 사용하면 12ns, finalizer를 사용하면 550ns ↔ 50배나 느려졌다.
- finalizer가 가비지 컬렉터의 효율을 떨어뜨리기 때문
- finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.
☑️ 그럼 대체 왜 만든거야 ???????
아마도 두 가지 쓰임새가 있다.
- 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할
- 클라이언트가 하지 않은 자원 회수를 늦게라도 하는게 안 하는 것보다는 낫기 때문
- FileInputStream, FileOutputStream, ThreadPoolExecutor 등에서 활용
- 네이티브 피어(native peer)와 연결된 객체에서 활용하기
- 네이티브 피어 : 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체 → 자바 객체가 아니므로 가비지 컬렉터의 대상이 되지 못한다.
공감이 하나도 안 되는 아이템ㅠ
[ item 9. try-finally보다는 try-with-resources를 사용하라 ]
전통적으로 InputStream, OutputStream 등 close 메서드를 호출해 직접 닫아줘야 하는 자원이 제대로 닫힘을 보장하는 수단으로 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블록 모두에서 발생할 수 있는데,
- try블록의 readLine()에서 예외가 발생 → 예외를 던지고 close메서드까지 실패하게 된다.
→ 두번째 예외가 첫번째 예외를 완전히 집어삼킴
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 방식은 너무 지저분하다.
☑️ try-with-resources 구문을 사용하자
: 단순히 void를 반환하는 close 메서드 하나만을 정의한 AutoClosable 인터페이스를 구현하여 사용
public static String firstLineOfFile(String path) throw IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
} catch (Exception e) {
return defaultVal;
}
}
- readLine과 close 양쪽에서 예외가 발생 → close 예외는 숨겨지고 readLine예외가 기록된다
- 하지만 숨겨진 예외들도 스택 추적 내역에 ‘숨겨졌다(suppressed)’ 꼬리표를 달고 출력된다.
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);
}
}
- 복수 자원 처리도 깔끔하다
- catch 구문 사용 가능 → try문을 더 중첩하지 않고도 다수의 예외 처리 가능
☑️ 핵심 정리
- 꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고 try-with-resources 구문을 사용하자.
- 코드는 짧고 예외 정보도 훨씬 유용하다.