아이템 8. finalizer와 cleaner 사용을 피하라. - ksw6169/effective-java GitHub Wiki
finalizer, cleaner는 기본적으로 쓰지 말아야 한다.
- 자바는 두 가지 객체 소멸자를 제공한다. (
finalizer
,cleaner
) - 그 중
finalizer
는 예측할 수 없고 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 하므로 일반적으로 불필요하다. - 따라서
finalizer
는 나름의 쓰임새가 몇 가지 있긴 하지만, 기본적으로 쓰지 말아야 한다. - 그래서 Java 9에서는
finalizer
를 deprecated API로 지정하고cleaner
를 그 대안으로 소개했다. 하지만cleaner
도 마찬가지로finalizer
보다 덜 위험할뿐 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.
왜 쓰지 말아야 하는가?
1. finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
-
객체에 접근할 수 없게 된 후
finalizer
와cleaner
가 실행되기까지 얼마나 걸릴지 알 수 없다. 즉,finalizer
와cleaner
로는 제때 실행되어야 하는 작업은 절대 할 수 없다. -
finalizer
와cleaner
가 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 천차만별이다. -
클래스에
finalizer
를 달아두면 그 인스턴스의 자원 회수가 제멋대로 지연될 수 있다.finalizer
스레드는 다른 애플리케이션 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못할 수 있다. 자바 언어 명세에서는 어떤 스레드가finalizer
를 수행할지 명시하지 않으니 이 문제를 예방할 보편적인 해법은 없다. 딱 하나가 있다면finalizer
를 사용하지 않는 것이다. -
한편
cleaner
는 자신을 수행할 스레드를 제어할 수 있다는 면에서 조금 낫다. 하지만 여전히 백그라운드에서 실행되며 가비지 컬렉터의 통제하에 있으니 즉각 수행되리라는 보장은 없다. -
자바 언어 명세는
finalizer
나cleaner
의 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다. 접근할 수 없는 일부 객체에 딸린 종료 작업을 전혀 수행하지 못한 채 프로그램이 중단될 수도 있다는 뜻이다. 따라서 프로그램 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대finalizer
나cleaner
에 의존해서는 안된다. 예를 들어 데이터베이스 같은 공유 자원의 영구 락 해제를finalizer
나cleaner
에 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것이다. -
System.gc
나System.runFinalization()
는finalizer
와cleaner
가 실행될 가능성을 높여줄 수는 있으나, 보장해주지는 않는다. 사실 이를 보장해주겠다는 메소드가 2개 있었다. 바로System.runFinalizersOnExit
와Runtime.runFinalizersOnExit
이다. 하지만 이 두 메소드는 심각한 결함 때문에 수십년간 지탄받아 왔다.
2. finalizer 동작 중 발생한 예외는 무시된다.
finalizer
동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.- 잡지 못한 예외 때문에 해당 객체는 자칫 마무리가 덜 된 상태로 남아있을 수 있으며 다른 스레드가 이처럼 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없다.
- 보통의 경우엔 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만,
finalizer
에서 발생한 예외는 경고조차 출력하지 않는다. - 그나마
cleaner
를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지 않는다.
3. finalizer는 가비지 컬렉터의 성능을 저하시킨다.
AutoCloseable
객체를 생성하고 가비지 컬렉터가 수거하기까지 12ns 소요되었다. 반면에finalizer
를 사용한 객체를 생성하고 파괴하니 50배나 느렸다.cleaner
도 클래스의 모든 인스턴스를 수거하는 형태로 사용하면 성능은finalizer
와 비슷하다. 하지만 안전망 형태로 사용하면 훨씬 빨라진다. 안전망 방식에서는 객체 하나를 생성, 정리, 파괴하는데 약 66ns가 소요된다.
4. finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 발생시킬 수 있다.
- 여기서
finalizer
공격이란, 생성자나 직렬화 과정에서 예외가 발생하면 이 생성되다만 객체에서 악의적인 하위 클래스의finalizer
가 수행될 수 있게 된다. - 이
finalizer
는 정적 필드에 자신의 참조를 할당하여 가비지 컬렉터가 수집하지 못하게 막을 수 있다. 이렇게 일그러진 객체가 만들어지고 나면 이 객체의 메소드를 호출하는 건 일도 아니다. - 객체 생성을 막으려면 생성자에서 예외를 던지는 것만으로 충분하지만,
finalizer
가 있다면 그렇지도 않다. - final 클래스들은 그 누구도 하위 클래스를 만들 수 없으니 이 공격에서 안전하다. final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는
finalize()
메소드를 만들고final
로 선언하자.
finalizer와 cleaner의 대안
- 파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체를 제대로 파괴하려면
finalizer
나cleaner
를 사용하는 대신에AutoCloseable
을 구현하거나 클라이언트에서 인스턴스를 다 쓰고 나면close()
를 호출해주면 된다. - 추가로 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다.
close()
에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메소드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면IllegalStateException
을 던지는 것이다.
finalizer와 cleaner의 적절한 용도
1. 자원의 소유자가 close() 를 호출하지 않는 것에 대비한 안전망 역할
finalizer
와cleaner
가 즉시 호출되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안하는 것보다는 낫다.- 안전망 역할의
finalizer
를 작성할 때는 그럴만한 값어치가 있는지 심사숙고하자. - 자바 라이브러리의 일부 클래스는 안전망 역할의
finalizer
를 제공한다. (ex.FileInputStream
,FileOutputStream
,ThreadPoolExecutor
)
2. 네이티브 피어(native peer)와 연결된 객체
- 네이티브 피어란 일반 객체가 네이티브 메소드를 통해 기능을 위임한 네이티브 객체를 말한다.
- 네이티브 피어는 자바 객체가 아니니 가비지 컬렉터는 그 존재를 알지 못한다. 그 결과 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못한다.
- 단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만
cleaner
와finalizer
를 사용하면 된다. 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 한다면close()
를 사용해야 한다.
참고 자료
- Effective Java 3/E
- A Guide to the finalize Method in JavaㅣBaeldung