티스토리 뷰

728x90
반응형

finalizer와 cleaner 사용을 피하라


자바에서 제공하는 두 가지 객체 소멸자는 finalizer와 cleaner입니다.

 

finalizer

  • finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요합니다.
  • 오작동, 낮은 성능, 이식성 문제의 원인이 되기도 하며, 기본적으로 사용하지 않는게 좋습니다.

cleaner

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

 

문제점


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

  • 객체에 접근할 수 없게된 후 finalizer나 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없습니다. 즉 finalizer와 cleaner는 제때 실행되어야 하는 작업은 절대 할 수 없습니다.
  • 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 천차만별입니다.

🧨 finalizer와 cleaner는 다른 애플리케이션 스레드보다 우선 순위가 낮아 실행 기회를 얻기 힘듭니다.

  • 위의 문제로 다른 스레드에게 우선 순위가 밀리다보니 심각할 경우 수천개의 객체들이 finalizer 대기열에서 대기만하다가 
    OutOfMemoryError가 발생하며 애플리케이션이 비정상 종료가 될 수 있습니다.

🧨 cleaner도 백그라운드에서 수행되며 가비지 컬렉터의 통제하에 있으니 즉각 수행된다는 보장은 없습니다.

 

🧨 수행시점 뿐 아니라 수행 여부조차 보장하지 않습니다.

  • 접근할 수 없는 일부 객체에 딸린 종료 작업을 전혀 수행하지 못한 채 프로그램이 중단될 수 있습니다. 따라서 프로그램 생애주기와
    상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer와 cleaner에 의존해서는 안됩니다.
  • System.gc나 System.runFinalization 메서드는 finalizer와 cleaner가 실행될 가능성을 높여줄 수는 있으나 보장해주진 않습니다.

🧨 finalizer 동작 중 발생한 예외는 무시됩니다.

  • 예외가 발생하고 처리할 작업이 남았더라도 그 순간 종료됩니다. 잡지 못한 예외 때문에 해당 객체는 자칫 마무리가 덜 된 상태로 남아 있을 수 있습니다. 그리고 다른 스레드가 이 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없습니다.

🧨 finalizer와 cleaner는 심각한 성능 저하 문제를 동반합니다.

 

🧨 finalizer 공격에 노출될 수 있는 보안 분제가 발생합니다.

  • 생성자나 직렬화 과정(readObject와 readResolve)에서 예외가 발생하면 해당 객체에서 악의적인 하위 클래스의 finalizer가 수핼될 수 있게 됩니다. finalizer로 어떻게 공격이 가능한걸까? << 이동
  • final이 아닌 클래스를 공격에서 방어하고 싶다면 아무일도 하지 않는 finalize 메서드를 만들고 final로 선언합니다.

 

대안책


💡 AutoCloseable을 구현한 뒤 close를 호출하도록 합니다.

 

💡 try-with-resources를 사용해 자동으로 close될 수 있도록 합니다.

 

위 두가지 방법을 사용하여 각 인스턴스는 자신이 닫혔는지 추적하는 것이 좋습니다. 다시 말해 close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렀다면 예외를 던지는 것입니다.

 

finalizer와 cleaner는 대체 어디에 쓰일까?


 

  • 자원의 소유자가 close 메서드를 호출하지 않는 것에 대한 안전망 역할입니다. cleaner나 finalizer가 즉시 호출되리라는 보장은 없지만 클라이언트가 하지 않는 자원 회수를 늦게라도 해주는 것이 아예 안하는 것보단 낫기 때문입니다. 이런 역할을 하는 자바 클래스는 FileInputStream, FileOutputStream, ThreadPoolExecutor가 대표적입니다.
  • 네이티브 피어와 연결된 객체 네이티브 피어는 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말합니다.
    네이티브 피어는 객체가 아니니 가비지 컬렉터가 그 존재를 알지 못합니다. 이때 자원회수에 사용하면 됩니다. 단 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당 됩니다. 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 한다면 close 메서드를 사용해야 합니다.

 

💡 예제 코드

public class Room implements AutoCloseable {

    private static final Cleaner cleaner = Cleaner.create();

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

	// 수거 대상이 된다면 방을 청소합니다.
    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles) {
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override
    public void close() throws Exception {
        cleanable.clean();
    }

    // 청소가 필요한 자원. 절대 Room을 참조해서는 안된다.
    private static class State implements Runnable {

        int numJunkPiles; // 방(Room)안의 쓰래기 수

        public State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }
		
        // close나 cleaner가 run 메서드를 호출합니다.
        @Override
        public void run() {
            System.out.println("방 청소");
            numJunkPiles = 0;
        }
    }
}

public class EffectiveJavaApplication {

    public static void main(String[] args) {

        // 아무리 기다려도 run() 메서드 호출 안됨
        new Room(99);
        System.out.println("아무렴");

        // run() 메서드 정상적으로 호출됨
        try (Room myRoom = new Room(7)) {
            System.out.println("안녕~");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

 

 

참고자료)

https://catsbi.oopy.io/d7f3a636-b613-453b-91c7-655d71fda2b1

https://jgrammer.tistory.com/entry/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C%EC%9E%90%EB%B0%94-finalizer%EC%99%80-cleaner-%EC%82%AC%EC%9A%A9%EC%9D%84-%ED%94%BC%ED%95%98%EB%9D%BC

https://yangbongsoo.tistory.com/8?category=919799 

https://github.com/java-squid/effective-java/issues/8

 

 

 

 

728x90
반응형