본문 바로가기

독서찰기(讀書札記)/이펙티브 자바

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

[Why]

  • finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있으니 기본적으로 '쓰지 말아야' 한다.
  • cleaner 역시 예측할 수 없고, 느리고, 일반적으로 불필요하다.
  • finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다. 보통의 경우에는 예외가 발생하여 스택 추적 내역을 출력하겠지만, finalizer에서는 경고조차 출력되지 않는다.
  • finalizer와 cleaner는 성능도 심각하다. 가비지 컬렉터보다 50배 정도 느리다.
  • finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다. 생성자나 직렬화 과정에서 만들어지다 만 객체를 악용할 수 있다.

[When]

파일 닫기

  • finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.
  • 그래서 파일 닫기를 finalizer나 cleaner에 맡기면 중대한 오류를 일으킬 수 있다. 시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문이다.

DB의 영구 lock 해제

  • 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다.
  • finalizer나 cleaner는 수행 시점뿐 아니라 수행 여부조차도 보장하지 않기 때문이다.

cleaner를 안정망으로 활용하는 AutoCloseable 클래스

※ 방(room) 자원을 수거하기 전에 반드시 청소(clean)해야 한다고 가정해보자.

public class Room implements AutoCloseable {

    private static final Cleaner cleaner = Cleaner.create();
    
    // 청소가 필요한 자원, 절대 Room을 참조해서는 안 된다!
    private static class State implements Runnable {
        int numJunkPiles;    // 방(Room) 안의 쓰레기 수
        
        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }
        
        // close 메서드나 cleaner가 호출된다.
        @Override
        public 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();
    }
    
}

 

  • State는 Runnable을 구현하고, 그 안의 run 메서드는 cleanable에 의해 딱 한 번만 호출될 것이다.
  • cleanable 객체는 Room 생성자에서 cleaner에 Room과 State를 등록할 때 얻는다.
  • State가 정적 중첩 클래스인 이유 : 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖는다아이템24. 그러면 순환참조가 생겨 가비지 컬렉터가 Room 인스턴스를 회수해갈 기회가 오지 않는다.
  • run 메서드가 호출되는 상황은 둘 중 하나다.  
    1. Room의 close 메서드를 호출할 때다. close 메서드에서 Cleanable의 clean을 호출하면 이 메서드 안에서 run을 호출한다.
    2. 가비지 컬렉터가 Room을 회수할 때까지 클라이언트가 close를 호출하지 않는다면, cleaner가 State의 run 메서드를 호출해줄 것이다.
// 잘 짜인 클라이언트 코드
public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("안녕~");
        }
    }
}

 

  • 기대한대로 Adult 프로그램은 "안녕~"을 출력한 후, 이어서 "방 청소"를 출력한다.
// 잘 못 짠 클라이언트 코드
public class Teenager {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("아무렴");
    }
}
  • "아무렴"에 이어 "방 청소"가 출력되지 않는다. 앞서 '예측할 수 없다'고 한 상황이다.

[How]

  • finalizer나 cleaner 대신, AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다. 일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resources를 사용해야 한다. 아이템9
  • 자바에서의 비메모리 자원 회수는 try-with-resources와 try-finally를 사용해 해결한다아이템9.
  • final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.