finalizer와 cleaner 사용을 피하라

finalizer, cleaner의 예측불가 문제

finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요한다. 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다. finalizer는 나름의 쓰임새가 몇 가지 있긴 하지만 기본적으로 쓰지말아야 한다.
자바9에서 finalizer를 deprecated API로 지정하고 cleaner를 소개했다. cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

finalizer와 cleaner는 즉시 수행된다는 보장이 없다. 객체에 접근할 수 없게 된 후 finalizer나 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없다. 즉, 제때 실행되어야 하는 작업은 절대 할 수 없다.

finalizer나 cleaner를 얼마나 신속히 수행할지는 전적으로 GC 알고리즘에 달렸으며, 이는 GC 구현마다 천차만별이다.
클래스에 finalizer를 달아두면 그 인스턴스의 자원 회수가 제멋대로 지연될 수 있다. finalizer 스레드는 다른 애플리케이션 스레드보다 우선순위가 낮아서 실행될 기회를 제대로 얻지 못한 것이다. 자바 언어 명세는 어떤 스레드가 finalizer를 수행할지 명시하지 않으니 이 문제를 예방할 보편적인 해법은 없다.
cleaner는 자신을 수행할 스레드를 제어할 수 있다는 면에서 조금 낫다. 하지만 여전히 백그라운드에서 수행되며 GC의 통제하에 있으니 즉각 수행되리라는 보장은 없다.

자바 언어 명세는 finalizer나 cleaner의 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다. 따라서, 프로그램 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다. 예를 들어 데이터베이스 같은 공유 자원의 영구 락 해제를 finalizer나 cleaner에 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것이다.

finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다. 잡지 못한 예외 때문에 해당 객체는 자칫 마무리가 덜 된 상태로 남을 수 있다. 보통의 경우 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만, 같은 일이 finalizer에서 일어난다면 경고조차 출력하지 않는다. 그나마 cleaer를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지 않는다.

finalizer, cleaner의 성능 문제

try-with-resources를 통해 객체를 생성하고 수거하는 것보다 finalizer를 사용한 객체를 생성하고 파괴하는 시간이 훨씬 오래 걸린다. finalizer가 GC의 효율을 떨어뜨리기 때문이다. cleaner도 클래스의 모든 인스턴스를 수거하는 형태로 사용하면 성능은 finalizer와 비슷하다.

finalizer, cleaner의 보안 문제

생성자나 직렬화 과정에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다. 이 finalizer는 정적 필드에 자신의 참조를 할당하여 가비지 컬렉터가 수집하지 못하게 막을 수 있다.
아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하면 공격으로부터 방어할 수 있다.

AutoCloseable

파일이나 스레드 등 종료해야 할 자원을 담고 있을 때 finalizer나 cleaner를 대신하여 사용할 수 있다.
AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다. (일반적으로 try-with-resources을 사용하면 close() 메서드가 호출됨)

public class Main {
    public static void main(String[] args) {
        try (CustomResource customResource = new CustomResource()){
            customResource.test();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

public class CustomResource implements AutoCloseable {

    public void test(){
        System.out.println("test");
    }

    @Override
    public void close() throws Exception {
        System.out.println("close");
    }
}

close 메서드 구현시 구체적인 Exception을 throw 하고, close 동작이 전혀 실패할 리 없을 때는 Exception을 던지지 않도록 구현하는 것이 좋다. 특히 close 메서드에서 InterruptedException을 던지지 않는 것을 강하게 권고한다. InterruptedException은 스레드의 인터럽트 상태와 상호작용 하므로 interruptedException이 억제되었을 때 런타임에서 잘못된 동작이 발생할 수 있기 때문이다.

finalizer, cleaner의 적절한 사용처

자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할

cleaner나 finalizer가 즉시 호출 되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 해준다. 이런 안정망 역할의 finalizer를 작성할 때는 그만한 값어치가 있는지 심사숙고해야한다.
자바 라이브러리의 일부 클래스는 안전망 역할의 finalizer를 제공한다.
FileInputStream, FileOutputStream, ThreadPoolExcutor

네이티브 피어(native peer)와 연결된 객체

네이티브 피어 : 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체
네이티브 피어는 자바 객체가 아니니 GC는 그 존재를 알지 못한다. 그 결과 네이티브 객체를 회수하지 못한다. 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에 finalizer나 cleaner를 사용하면 된다. 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 한다면 close 메서드를 사용해야 한다.

cleaner를 안전망으로 활용

public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
    private static class State implements Runnable {
        int numJunkPiles; // Number of junk piles in this room

        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }

        // close 메서드나 cleaner가 호출한다.
        @Override public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }

    // 방의 상태. cleanable과 공유한다.
    private final State state;

    // cleanable 객체. 수거 대상이 되면 방을 청소한다.
    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의 run 메서드는 cleanable에 의해 딱 한번 호출된다. 이 cleanable 객체는 Room 생성자에서 cleaner에 Room과 State를 등록할 때 얻는다. run 메서드가 호출되는 상황은 Room의 close 메서드가 호출되거나 GC가 Room을 회수할 때까지 클라이언트가 close를 호출하지 않는다면, cleaner가 State의 run 메서드를 호출해줄 것이다.
State 인스턴스는 절대 Room 인스턴스를 참조해서는 안 된다. Room 인스턴스를 참조할 경우 순환참조가 생겨 GC가 Room 인스턴스를 회수해갈 기회가 오지 않는다. 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖게 되므로 State는 정적 중첩 클래스여야 한다.

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

Comments