본문 바로가기
SW LAB/Java

Effective Java : (9) Finalizer와 Cleaner의 단점

by 프롬스 2020. 4. 24.
반응형

Finalizer와 Cleaner 사용을 피해라

자바는 두 가지 객체 소멸자를 제공합니다.
그 중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요합니다.
오동작, 낮은 성능, 이식성 문제의 원인이 되기도 합니다.

자바 9에서는 finalizerdeprecated API로 지정하고 cleaner를 그 대안으로 소개하고 있습니다.
cleanerfinalizer보다 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요합니다.

finalizercleaner는 즉시 수행된다는 보장이 없기 때문에 제 때 실행되어야 하는 작업은 절대 할 수 없습니다.
파일 닫기를 수행하는 것을 finalizercleaner에 맡긴다면 중대한 오류를 발생시키게 됩니다.
시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문입니다.
finalizercleaner가 신속시 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달려있고, 가비지 컬렉터 구현마다 천차만별입니다.

상태를 영구적으로 수정하는 작업에서는 절대 finalizercleaner에 의존해서는 안됩니다.
데이터베이스 같은 공유 자원의 영구 락 해제를 finalizercleaner에 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것입니다.

System.gcSystem.runFinalization 메서드도 finalzercleaner가 실행될 가능성을 높여줄수는 있어도 보장해주지는 않습니다.

  • finalizer와 cleaner는 심각한 성능 문제도 동반합니다.
    AutoCloseable 객체를 생성하여 가비지 컬렉터가 객체를 수거하기 까지 12ns가 걸렸다면, finalizer는 550ns 즉 50배가 느린 성능을 보여줍니다.
    finalzer가 가비지 컬렉터의 효율을 떨어뜨리기 때문입니다.

  • finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있습니다.
    생성자나 직렬화 과정에서 예외가 발생하면, 생성 되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 됩니다.
    finalizer에 정적 필드를 생성하여 가비지 컬렉터가 수집하지 못하도록 할 수 있기 때문이죠.
    객체 생성을 막으려면 생성자에서 예외를 던지는 것만으로 충분하지만 finalizer가 있다면 그렇지도 않습니다.
    final 클래스는 하위 클래스를 만들 수 없어 괜찮지만 이 외에는 finalize 메서드를 final로 선언하여 방어해야 합니다.

그렇다면 finalizercleaner를 대신해줄 묘안은 무엇이 있을까요 ?
AutoCloseable를 구현해주고, 사용이 끝난 객체는 close()를 호출하도록 하면 됩니다.

finalizercleaner의 쓰임새는 두 가지 정도가 있습니다.

  1. 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할
    자바 라이브러리의 일부 클래스도 이 역하을 수행하고 있습니다.
    대표적으로 FileInputStream, FileOutputStream, ThreadPoolExecutor 가 있습니다.

  2. 네이티브 피어(native peer)와 연결된 객체
    네이티브 피어란 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말합니다.
    네이티브 피어는 자바 객체가 아니니 가비지 컬렉터가 그 존재를 알지 못합니다.
    이 때, finalizercleaner가 나서서 처리하기에 적당한 작업입니다.
    단, 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 한다면 앞서 설명한 close 메서드를 사용해야 합니다.

사용자가 정의한 Room 클래스에 cleaner가 구현되어 있고 cleaner가 호출될 때 "방청소"가 출력된다고 가정하고 다음 코드를 봅시다.

public class Tennager {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("아무렴");
    }
}

위 코드에서 "아무렴"은 출력되지만 "방청소"는 출력되지 않고 종료되는 모습을 확인할 수 있을 것입니다.
System.gc()를 종료 전 호출했다면, "방청소"가 보일 수도 있고, 안보일 수도 있습니다. (즉시 호출을 보장하지 않기 때문에)

System.exit을 호출할 때의 cleaner 동작은 구현하기 나름이다. 청소가 이뤄질지는 보장하지 않는다.

public class Tennager {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("안녕");
        }
    }
}

위 코드에서는 "안녕"에 이어서 "방청소"가 출력되는 것을 볼 수 있을 것입니다.
try-with-resource 블록으로 감쌌기 때문에 자동 청소가 필요하지 않기 때문이지요.

핵심 정리
cleaner(자바 8까지는 finalizer)는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자.
물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.

반응형

댓글