아이템 8. finalizer와 cleaner(자바 8 이전 : finalizer, 8 이후 : cleaner 즉 동일) 사용을 피하라
finalizer는 오동작, 낮은 성능, 이식 문제를 야기할 수 있다. 그래서 자바 9부터는 deprecated 되었으며, cleaner가 그 대안으로 소개된다. 하지만, cleaner도 여전히 느리고, 예측할 수 없고, 일반적으로 불필요하다
C++의 destructor와는 다른 개념으로, 자바는 가비지 컬렉터가 있어 자원 회수에 프로그래머의 역할이 필요없다. 비 메모리 자원 회수를 위해서는 자바에 try-with-resources와 try-finally가 있다
finalizer와 cleaner는 즉시 수행된다는 보장이 없고, 가비지 컬렉터 구현에 따라 수행속도가 가지각색이다. 클래스에 finalizer가 있는 경우 인스턴스의 자원회수를 보장할 수 없다. 또한 finalizer는 자신을 수행할 스레드를 제어할 수 없어 지연된 경우 한 없이 대기하다가 OOM 위험을 증가시킨다. cleaner는 그래도 스레드 제어가 가능하지만 여전히 백그라운드에서 가비지 컬렉터의 통제 하에 있으므로 즉각 수행된다는 보장이 없다
자바는 finalizer나 cleaner의 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다. 그러므로 프로그램 수명주기와 관련이 없는 상태를 영구적으로 수정하는 작업에서 finalizer나 cleaner를 의존해서는 안된다(DB 영구 락 해제를 의존하는 경우 해당 분산 시스템 전체가 멈추게 된다)
System.gc 또는 System.runFinalizer 같은 메서드가 있지만, 실행을 보장하는 것은 아니다. 보장하는 메서드로는, System.runFinalizersOnExit 메서드와 Runtime.runFinalizersOnExit 메서드가 있다. 다만 심각한 결함을 가지고 있어 사용이 매우 제한된다
finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남아있어도 그 즉시 종료된다. 예외를 잡을 수가 없으므로, 그 객체의 작업이 완전히 완료된 것인지를 확신할 수 없다. 그리고 이 불완전한 객체를 다른 스레드가 사용하려고 하면, 어떻게 동작할 지 예측할 수가 없다. 일반적으로(finalizer를 사용하지 않은 경우) 잡지 못한 예외가 이 때 등장해 스레드를 중단하지만, finalizer로 인해 발생한 예외는 경고를 출력하지 않고 스레드를 중단하지도 않는다. cleaner의 경우는 자신의 스레드를 통제하기 때문에 이 문제는 해당하지 않는다
심각한 성능 문제도 발생하는데, finalizer로 생성한 객체는 가비지 컬렉터의 비효율을 발생시키므로, try-with-resource에 비교해 약 50배 느려진다. cleaner의 경우도 클래스의 모든 인스턴스를 수거하는 형태로 사용하면 유사 퍼포먼스를 보인다. 안전망 형태로 사용하는 경우 보다 개선되나, 여전히 5배 정도 느리다
finalizer 객체의 경우, finalizer 공격에 노출되어 심각한 보안문제를 일으킬 수도 있다. 생성자나 직렬화 과정에서 예외가 발생할 경우, 생성되다 만 객체의 하위 클래스에서 finalizer가 수행될 수 있다. 정적 필드에 자신의 참조를 할당해 GC가 수거하지 못하도록 만들 수 있으며, 이는 메모리 사용률를 저하시킨다. 객체 생성을 막기 위해 생성자에서 예외를 던질 수 있지만, finalizer의 경우 그렇지 못하다. final이 아닌 클래스를 finalizer 공격으로부터 방어하려면, 아무 일도 하지않는 finalize 메서드를 만들고 final로 선언하면 된다
파일이나 스레드 등 종료해야 하는 자원을 담고 있는 객체의 클래스에서 finalizer나 cleaner를 대신할 묘안으로, AutoCloseable을 구현한 후, 인스턴스의 사용이 끝나면 close();를 호출한다(일반적으로 try-with-resource를 활용한다). 각 인스턴스는 자신이 닫혔는지를 추적하면 좋다
-> close() 메서드에서 이 객체는 더 이상 유효하지 않다는 것을 필드에 표시하고, 다른 메서드는 이 필드를 검사해 유효한지의 여부를 판단하는 것이다. 유효하지 않은 경우 IllegalStateException을 던지게 구현하면 된다.
finalizer와 cleaner의 적절한 사용처는 두 가지 정도가 있다. 하나는 자원 소유자가 close 메서드를 호출하지 않는 경우에 대비한 안전망 역할이다. cleaner나 finalizer가 즉시(혹은 끝까지) 호출되리라는 보장은 없지만, 클라이언트가 자원 회수를 하지 않는 경우에, 늦게라도 하는 것이 낫다는 측면이다. 자바 라이브러리 일부 ~ FileInputStream, FileOutputStram, ThreadPoolExecutor는 이와 같은 역할로 finalizer를 사용한다
두번째로, 네이티브 피어(native peer)와 연결된 객체에서 사용한다. 네이티브 피어란, 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다. 자바 객체가 아니기 때문에 가비지 컬렉터에 의해 자원이 회수될 수가 없으므로, cleaner나 finalizer를 통해 자원을 회수해야 한다. 단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않은 경우에 해당한다(해당하지 않는 경우 close()메서드를 사용한다)
cleaner를 안전망으로 구현하기 위해 AutoCloseable을 구현하는 Room 클래스를 예제로 들면, Room 자원을 수거하기 전, 반드시 clean해야 한다고 할 경우, cleaner를 public API가 아니게 구현할 수 있다
public class Room implements AutoCloseable {
private static fianl Cleaner cleaner = Cleaner.create();
// 청소가 필요한 자원, 절대 Room을 참조하면 안된다
private static class State implements Runnalbe {
int numJunkPiles; // 방 안의 쓰레기 수
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// close 메서드나 cleaner가 호출한다
@Override public void run() {
System.out.println("방 청소");
numJunkPiles = 0;
}
}
// 방의 상태, cleanable과 공유한다
private final State state;
// cleanerble 객체. 수거 대상이 되면, 방을 청소한다
private final Cleaner.Cleanable cleanable;
public Room(int numJunkPiles) {
state = new State(numJunkPiles);
cleanable = cleaner.register(this, state);
}
@Override public void close() {
cleanable.clean();
}
}
.내부 클래스인 State는 Room 객체의 자원이 회수될 때 cleaner의 대상인 수거할 자원을 담고 있다(numJunkFiles). 네이티브 피어의 경우 이 자원이 final long 변수가 된다. State는 Runnable을 구현하고, 그 안의 run 메서드는 cleanable에 의해 딱 한번 호출된다. 이 cleanable 객체는 Room 생성자에서 cleaner에 Room과 State를 등록할 때 얻는다. run 메서드가 호출되려면, 보통은 Room의 close()가 호출될 때이다. 또는 가비지 컬렉터가 Room을 수거할 때까지 close()가 호출되지 않으면, 그 때 cleaner가 State의 run()을 호출(하는 것이 기대된다)
State 인스턴스는 절대로 Room 인스턴스를 참조해서는 안된다. 순환참조가 발생하기 때문에 가비지 컬렉터에 의해 자원이 수거될 수가 없다. State를 내부 static으로 선언한 이유가 이 것이다. static이 아닌 경우, 내부 클래스는 자동으로 바깥 객체의 참조를 가지기 때문이다(아이템 24). 이와 비슷하게 람다 역시 바깥 객체 참조를 가지기
쉬우므로 사용하지 않는 것이 좋다
try-with-resource로 감싸는 경우 위와 같은 "자동 청소"는 필요하지 않다.
public class Adult {
public static void main(Strina[] args) {
try(Room myRoom = new Room(7)) {
System.out.println("안녕~");
}
}
}
Adult 프로그램은 Room의 자원 회수를 성공적으로 수행한다. "안녕~"이 출력된 후 "방 청소"까지 잘 출력된다.
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.printlin("아무렴");
}
}
System.exit을 호출할 때 cleaner는 그 동작이 보장되지 않기 때문에 해당 Room 인스턴스에 대한 자원 회수는 보장할 수 없다. main 메서드에 System.gc() 메서드를 추가해 자원 회수를 시도할 수 있으나, 이 역시 보장되는 것은 아니다
'자바☕ > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 읽고 정리해보기 10-1 (0) | 2025.02.11 |
---|---|
이펙티브 자바 읽고 정리해보기 9. (0) | 2024.10.27 |
이펙티브 자바 읽고 정리해보기 7. (0) | 2024.08.31 |
이펙티브 자바 읽고 정리해보기 6. (0) | 2024.08.23 |
이펙티브 자바 읽고 정리해보기 4 & 5. (0) | 2024.08.15 |