자바에서는 객체 소멸자로 finalizer와 cleaner를 제공하지만, 모두 예측할 수 없고, 상황에 따라서는 위험할 수도 있다.
(자바 9에서는 finalizer를 Deprecated로 정하고, 대안으로 cleaner를 냈지만, cleaner도 역시 예측할 수 없다.)
finalizer와 cleaner 모두 즉시 실행된다는 보장이 없고. 언제 실행될지 알 수 있는 방법이 없기 때문에 finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.
즉 요약하자면, 상태를 영구적으로 수정하는 작업에서는 절때 finalizer와 cleaner에 의존해서는 안 된다는 것이다.
예를 들면 데이터베이스 같은 공유 자원의 영구 락(lock)의 해제를 finalizer와 cleaner에 맡겨 놓으면 안된다.
또한 finalizer의 작업 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.
또한 finalizer와 cleaner는 성능에도 심각한 악영향을 끼친다.
try-catch-resourses로 자원을 회수하는 거에 비해 성능이 약 5배정도 떨어진다.
또한 finalizer를 사용한 클래스는 finalizer 공격[https://yangbongsoo.tistory.com/8]에 노출되어 보안 문제를 일으킬 수도 있다.
만약 final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.
그렇다면 파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 finalizer와 cleaner를 대신해줄 방법은 무엇일까?
바로 AutoCloseable을 구현해준 후, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다.
그렇다면 finalizer와 cleaner는 대체 어디에 쓰일까?
하나는 자원의 소유가자 close 메서드를 호출하지 않는 것에 대비한 안전망 역할이다.
finalizer와 cleaner가 즉시 호출된다는 보장은 없으나, 클라이언트가 하지 않은 자원 회수를 늦게라도 하는 것이 하지 않는 것보다는 나으니 말이다.
자바 라이브러리의 일부 클래스는 안전망 역할의 finalizer를 제공하는데, FileInputStream, FileOutputStream, ThreadPoolExcecutor가 대표적이다.
두 번째 예시는 네이티브 피어(Native peer)와 연결된 객체에서이다.
네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다.
네이티브 피어는 자바 객체가 아니므로 가비지 컬렉터는 그 존재를 알지 못하고, 따라서 해당 객체를 회수하지 못한다.
cleaner는 사용하기에 조금 까다로운데, 다음 예시를 들어 이 기능을 설명해보겠다.
import java.lang.ref.Cleaner;
public class Room implements AutoCloseable{
private static final Cleaner cleaner = Cleaner.create();
private static class State implements Runnable {
int numJunkPiles;
State(int numJunkPiles){
this.numJunkPiles = numJunkPiles;
}
@Override
public void run() {
System.out.println("방 청소");
numJunkPiles = 0;
}
}
private final State state;
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override
public void close(){
cleanable.clean();
}
}
static으로 선언된 중첩 클래스인 State는 cleaner가 방을 청소할 때 수거할 자원들을 담고있다.
이 예에서는 단순히 방 안에 쓰레기 수를 뜻하는 numJunkPiles 필드가 수거될 자원에 해당한다.
State는 Runnable을 구현하고, 그 안의 run 메서드는 cleanable에 의해 딱 한번만 호출될 것이다.
이 cleanable 객체는 Room 생성자에서 cleaner에 Room과 State를 등록할 때 얻는다.
run 메서드가 호출되는 상황은 둘 중 하나인데, 보통은 Room의 close 메서드를 호출할 때이다.
close 메서드에서 cleanable의 clean을 호출하면 이 메서드 안에서 run을 호출한다.
혹은 가비지 컬렉터가 Room을 회수할 때 까지 클라이언트가 close를 호출하지 않는다면, cleaner가 State의 run 메서드를 호출해줄 것이다.
State 인스턴스는 절대로 Room 인스턴스를 참조해서는 안된다.
만약 Room을 참조한다면 순환참조가 생겨버려서 가비지 컬렉터가 Room 인스턴스를 회수해갈 기회가 오지 않는다.
Room을 정적(static) 중첩 클래스로 만든 이유가 바로 이것인데, 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 되기 때문이다.(아이템 24)
이와 비슷하게 람다 역시 바깥 객체의 참조를 갖기 쉬우니 사용하지 않는 것이 좋다.
올바른 사용은 다음과 같다.
public class Adult {
public static void main(String[] args) {
try (Room room = new Room(7)){
System.out.println("Hi~");
}
}
}
아무튼 이번 아이템을 정리하면 다음과 같다.
cleaner(자바 8까지는 finalizer)는 안전망 역할이나, 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.
'☕️ Java > 이펙티브 자바' 카테고리의 다른 글
[Effective Java] 아이템 10 - equals는 일반 규약을 지켜 재정의하라 (0) | 2022.02.05 |
---|---|
[Effective Java] 아이템 9 - try-finally 보다는 try-with-resources를 사용하라 (0) | 2022.02.04 |
[Effective Java] 아이템 7 - 다 쓴 객체 참조를 해제하라 (0) | 2022.02.03 |
[Effective Java] 아이템 6 - 불필요한 객체 생성을 피하라 (0) | 2022.02.03 |
[Effective Java] 아이템 5 - 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.02.02 |