DEV ℧ Developer Diary

[EffectiveJava] item08 - finalizer와 cleaner 사용을 피해라.

finalizer와 cleaner 사용을 피해라.

finalizercleaner은 자바에서 제공하는 객체 소멸자이다. 하지만 이름이 무색하게도 해당 객체들은 크게 쓰이지 않는다.

@Override
public void finalize() {
    ...
}

finalize 메서드를 override하면 해당 객체가 jvm에게 GC을 해야할 대상이 될때 호출되지만,

finalizer는 예측할 수 없고 상황에따라 위험할 수 있기 때문에 일반적으로는 불필요하다. 오동작, 낮은 성능, 이식성 문제의 원인이 되기도한다.

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

왜냐하면, 둘의 호출로 인해 쉽게 객체를 정리 할 수 있다면 좋은것이 아니냐? 라는 물음을 가질 수 있지만 사실과는 다르다.

finalizer와 cleaner로는 실행되기까지 얼마나 걸릴지 알 수 없기 때문에 finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.

그 이유는, 백그라운드에서 실행되고 가비지컬렉터의 알고리즘에 달렸으며, 가비지컬렉터마다 알고리즘의 구현이 천차만별이기 때문이다. 또한 finalizer의 스레드가 다른 애플리케이션의 스레드보다 우선 순위가 낮기 때문에 실행될 기회를 제대로 얻지 못한다. 한편, cleaner는 자신을 수행할 스레드를 제어할 수 있다는 면에서 조금은 낫지만, 여전히 즉각 수행되리라는 보장은 없다.

cleaner는 AutoCloseable를 구현하여 사용할 수 있다.

import java.lang.ref.Cleaner;

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

    /* 청소가 필요한 자원 순환 참조를 방지하기 위해 Room을 참조하면 안된다.
     * 순환참조가 일어나면 GC가 회수할 수 없다. */
    private static class State implements Runnable {
        int numJunkPiles;

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

        /* close메서드나 clener가 호출한다. */
        @Override
        void run() {
            System.out.println("방 청소");;
            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();
    }
}

cleanable객체는 Room 생성자에서 cleanerRoomState를 등록할 때 얻는다. run()메서드가 실행되는 상황은 둘 중 하나다.

  1. 보통 Roomclose() 메서드를 호출할 때다.
  2. 가비지 컬렉터가 Room을 회수할 때까지 클라이언트가 close()를 호출하지않으면 cleanerStaterun() 메서드를 호출한다.

이렇게 cleaner는 단순히 Room 객체의 안전망으로 쓰인것이다.

해당 코드들은 다음과 같이 개선될 수 있다. try-with-resource 블록을 사용했다.

try-with-resource는 try에 자원 객체를 전달하면, try 코드 블록이 끝나면 자동으로 자원을 종료해주는 기능이다.

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

위의 코드는 “안녕~”을 출력한 후, 이어서 Room객체 내부의 “방 청소”를 출력한다.

만약 cleaner를 사용한다면 아래와 같다.

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

위의 코드는 “방 청소”는 한번도 출력되지 않는다, 앞선 언제 실행될지 모르는 ‘예측할 수 없는 상황이다.’

cleaner(자바 8까지는 finalizer)는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 하지만 이 경우에도 불확실성과 성능저하에는 주의하도록 하자