본문 바로가기
이펙티브 자바

[아이템 8] finalizer와 cleaner 사용을 피하라

by 에드박 2021. 4. 24.

자바는 finalizer, cleaner 두 가지 객체 소멸자를 제공합니다.

 

fianlizer

finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어서 일반적으로 불필요합니다.

-> 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 합니다. 나름의 쓰임새가 있지만 기본적으로는 쓰지 않는것이 좋습니다.

 

자바9에서는 finalizer 사용을 Deprecated API로 지정하고 cleaner 사용을 대안으로 소개하고 있습니다.
(하지만 자바 라이브러리에서도 여전히 finalizer를 사용하고 있습니다.)

 

cleaner

finalizer 보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요합니다.

 

C++ 의 파괴자(destructor)와는 다른 개념입니다.
C++의 파괴자는 특정 객체와 관련된 자원을 회수하는 보편적인 방법

자바에서는 보통 try-with-resource 또는 try-finally 로 해결합니다.

 


finalizer 나 cleaner 의 수행 시점에 대한 문제점

 

finalizer 와 cleaner 는 즉시 수행된다는 보장이 없습니다.

객체에 접근할 수 없게된 후 finalizer나 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없습니다.

 

즉, finalizer 와 cleaner 로는 제때 실행되어야 하는 작업은 절대 할 수 없습니다.

finalizer 와 cleaner 가 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 천차만별입니다.

 

클래스에 finalizer 를 달아두면 그 인스턴스의 자원 회수가 제멋대로 지연될 수 있습니다.

자바 언어 명세는 어떤 스레드가 finalizer를 수행할지 명시하지 않으니 finalizer 스레드가 다른 애플리케이션 스레드보다 우선순위가 낮아서 실행될 기회를 계속 얻지 못할 수 있습니다. 결국 finalizer 가 대기열에 쌓여 OutOfMemoryError를 발생시킬 수 있습니다.

이 문제를 예방할 보편적인 해법은 없습니다. 단 하나, finalizer를 사용하지 않는 방법 뿐입니다.

 

cleaner는 자신을 수행할 스레드를 제어할 수 있다는 면에서 조금 낫습니다. 하지만 여전히 백그라운드에서 수행되며 가비지 컬렉터의 통제하에 있으니 즉각 수행되리라는 보장은 없습니다.

 


finalizer 나 cleaner 의 수행 여부의 문제점

자바 언어 명세는 finalizer나 cleaner의 수행 시점뿐 아니라 수행 여부조차 보장하지 않습니다.

접근할 수 없는 일부 객체에 딸린 종료 작업을 전혀 수행하지 못한 채 프로그램이 중단될 수도 있다는 얘기입니다.

 

따라서 프로그램의 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안됩니다.

 

예를 들어 데이터베이스 같은 공유 자원의 영구 락(lock) 해제를 finalizer나 cleaner에 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것입니다.

 


끝나지 않는 finalizer의 부작용

finalizer 동작 중 발생한 예외는 무시되며 처리할 작업이 남았더라도 그 순간 종료됩니다.

잡지 못한 예외 때문에, 해당 객체는 자칫 마무리가 덜 된 상태로 남을 수 있습니다.

 

다른 스레드가 이처럼 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없습니다.

보통의 경우엔 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만, 같은 일이 finalizer에서 일어난다면 경고조차 출력하지 않습니다.

 

그나마 cleaner를 사용하는 라이브러리는 자신의 스레드를 통제하기 떄문에 이러한 문제가 발생하지 않습니다.

 


finalizer 와 cleaner 의 심각한 성능 문제

내 컴퓨터에서 간단한 AutoCloseable 객체를 생성하고 가비지 컬렉터가 수거하기까지 12ns가 걸립니다.(try-with-resource)

finalizer를 사용하면 550ns가 걸립니다.

 

다시말해서 finalizer를 사용한 객체를 생성하고 바괴하니 50배나 느렸다는 의미입니다.

cleaner도 클래스의 모든 인스턴스를 수거하는 형태로 사용하면 성능은 finalizer와 비슷합니다.

finalizer 사용에 대한 보안 문제

finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 도 있습니다.

finalizer 공격 원리는 간단합니다. 생성자나 직렬화 과정에서 예외가 발생하면, 이 생성되다 중단된 객체는 악의적인 하위 클래스의 finalizer가 수행될 수 있게 됩니다.

 

객체 생성을 막으려면 생성자에서 예외를 던지는것 만으로 충분하지만, finalizer가 있다면 그렇지 못합니다.


finalizer 나 cleaner의 대안 AutoCloseable

파일이나 스레드 등 종료해야할 자원을 담고 있는 객체의 클래스에서 finalizer 나 cleaner 대신

 

그저 AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close메서드를 호출하면 됩니다.

일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resource를 사용해야 합니다.

 

구체적인 구현법에서 알아두면 좋은게 하나 있습니다.

close() 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던지는 것입니다.


그렇다면 finalizer와 cleaner는 언제 쓰는것인가?

적절한 쓰임새가 2가지 있습니다.

  • 하나는 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할
    • cleaner나 finalizer가 즉시(혹은 끝까지) 호출되리라는 보장은 없지만, 클라이언트가 하지않은 자원 회수를 늦게라도 해주는 것이 안해주는것 보다는 좋기 때문입니다.
    • 이런 안전망 역할의 finalizer를 작성할 때는 성능상의 이슈를 감당할만한 값어치가 있는지 먼저 생각해봐야 합니다.
    • 자바 라이브러리의 일부 클래스는 안전망 역할의 finalizer를 제공합니다.
    • 예) FileInputStream, FilerOutputStream, ThreadPoolExecutor
  • 두번째는 네이티브 피어(native peer)와 연결된 객체에서 입니다.
    • 네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말합니다.
    • 네이티브 피어는 자바 객체가 아니니 가비지 컬렉터가 그 존재를 알지 못합니다.
    • cleaner나 finalizer가 나서서 하기 적당한 작업입니다.
    • 단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 떄에만 해당됩니다.

댓글