[이펙티브 자바] 아이템8 | finalizer와 cleaner 사용을 피하라

제롬·2022년 1월 10일
0

이펙티브자바

목록 보기
8/25

자바가 제공하는 객체 소멸자 - finalizer와 cleaner

  • finalizer

    • finalize 메서드는 최상위 Object 클래스에 포함된 메서드이다. 그래서 어느 클래스던지 finalize 메서드를 재정의 할 수 있다.

    • finalize메서드를 재정의(Overriding)하면 해당 객체가 Garbege Collection 대상이 되었을 때 finalize메서드가 호출된다. 단, 즉시 호출을 보장받을 수는 없다.

    • 즉시 호출이 보장되지 않기 때문에 한정적 자원 해제시 해제하는 작업을 finalizer로 구현하면 안된다. 이런 경우 try-with-resource 또는 try-finally를 이용해 구현해야 한다.

    • finalize 메서드는 자바9부터 deprecated 되었고 이에 대한 대안으로 자바에서는 Cleaner를 지원하게 되었다.

  1. cleaner
    자바 9부터 finalizer 대신 새로운 소멸자인 java.lang.ref 패키지에 포함된 cleaner를 대안으로 제공한다. cleaner는 API로 제공했던finalizer처럼 재정의(Overriding)하는 것과 달리 구성을 통해 cleaner를 사용해야 한다. 하지만, cleaner 역시 예측할 수 없고, 느리고, 일반적으로 불필요하다.

finalizer와 cleaner의 사용은 지양하라.

첫 번째, finalizer 와 cleaner 는 즉시 실행된다는 보장이 없다.

finalizercleaner 는 즉시 실행된다는 보장이 없어 제때 실행되어야하는 작업은 절대 할 수 없다.

  • 언제 실행될지 알 수가 없다. 어떤 객체가 필요 없어진 시점에 finlizer 또는 cleaner가 바로 실행되지 않을 수도 있다. 그 시간이 얼마나 걸릴지는 아무도 모른다. 따라서 타이밍이 중요한 작업을 절대로 finalizer 또는 cleaner로 해서는 안된다.

  • 예를 들어, 파일 리소스를 반납하는 작업을 finalizer 또는 cleaner로 처리한다면 실제로 그 파일 리소스가 언제 처리될지 알 수 없고 자원 반납이 안되서 더 이상 새로운 파일을 열 수 없는 상황이 발생할 가능성이 있다.

두 번째, finalizer 처리는 인스턴스의 자원 회수가 제멋대로 지연될 수 있다.

인스턴스 회수가 지연될 뿐만 아니라 제대로 실행되지 않을수도 있다.

  • finalizer 스레드는 다른 애플리케이션 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못할수도 있다. (다른 작업이 바쁘면 뒤로 미뤄진다, 언제 호출될지 모른다는 의미이다.) 따라서 finlizer 안에 어떤 작업이 있고, 그 작업을 쓰레드가 처리 못해서 대기하고 있다면 해당 인스턴스는 GC가 되지 않고 계속해서 쌓이다가 결국 OOM(Out Of Memory Exception)이 발생할 수 있다.

  • cleaner 는 자신을 수행할 스레드를 제어할 수는 있지만, 역시나 가비지 컬렉터에 의존하므로 즉시 사용된다는 보장은 없다.

세 번째, 수행 여부조차 보장되지 않는다.

상태를 영구적으로 수정하는 작업에서는 절대 finalizercleaner 에 의존해서는 안된다.

  • 자바 언어 명세는 finalizerclenaer 의 수행 여부를 보장하지 않는다. 접근할 수 없는 일부 객체에 딸린 종료작업을 수행하지 못한채 프로그램이 종료될 수도 있다.

  • 만약 데이터 베이스 같은 자원의 락을 이것들로 반환하는 작업을 한다면 전체 분산 시스템이 멈춰 버릴 수도 있다. 따라서 finalizerclenaer로 저장소 상태를 변경하는 일을 하지 말아야 한다.

public class SampleRunner {
    public static void main(String[] args) {
        final SampleRunner sampleRunner = new SampleRunner();
        sampleRunner.run();
        System.gc(); // 실행이 안될수도 있다!
    }
}
  • System.gcSystem.runFinalization 메서드에 속지 말자 이들은 실행 가능성을 높여줄 순 있으나, 보장하지는 않는다.

  • System.runFinalizationOnExitRuntime.runFinalizationOnExit 는 실행을 보장해주려 만들었지만 심각한 결함으로 수십년간 deprecated 상태이다.

네 번째, 예외가 무시된다.

finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.

잡지못한 예외때문에 객체가 훼손될 수 있고, 다른 스레드가 이 훼손된 객체에 접근하게 될 수 있다. finalizer 에서 예외가 발생할 경우 경고조차 출력하지 않는다. 그나마 cleaner 를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지 않는다.

다섯 번째, 심각한 성능 문제도 동반한다.

  • finalizer 가 가비지 컬렉터의 효율을 떨어뜨리기 때문에 try-with-resource 와 비교했을 때 굉장히 느리다. cleaner 역시 마찬가지이다.

여섯 번째, finalizer는 심각한 보안문제를 일으킬 수 있다.

  • finalizer 를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있다.

  • A, B 클래스가 있다고 가정했을 때, B클래스가 A클래스를 상속받는다. 그리고 finalize 를 오버라이딩한 B 클래스의 인스턴스를 생성하는 도중에 예외가 발생하거나, 직렬화 과정에서 예외가 발생하면, 원래 죽었어야 할 객체의 finalizer가 실행될 수 있다.
    그럼 finalize 메서드 안에서 이 인스턴스가 가진 static 필드에 접근할 수 있어서 해당 인스턴스의 레퍼런스를 기록할 수 있고 GC에 의해 수거되지 못하게 할 수도 있다.
    원래는 생성자가 예외를 발생시켜 존재하지 않았어야 하는 인스턴스인데 finalizer 때문에 살아남아 있는 것이다.

  • 이 문제에 대한 해결방법은 final 클래스들은 하위 클래스를 만들 수 없으니 공격으로부터 안전하다. 따라서 아무일도 하지 않는 finalizer 메서드를 만들고 final 로 선언하자.

finalizer 와 cleaner를 정상적으로 활용하는 방법

파일이나 스레드등 종료해야 할 자원을 담고 있는 객체의 클래스에서 AutoCloseable을 구현해 주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close()를 호출하면 된다.

일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resource를 사용해야 한다. 각 인스턴스는 자신이 닫혔는지 추적하기 위해 close() 메서드 호출 여부를 필드로 저장한다. close() 메서드에서는 이 객체가 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 호출되었다면 IllegalStateException 을 던지도록 구현한다.

첫 번째, try-with-resource를 이용한 자원 반납 방법

public class SampleResource implements AutoCloseable{
    public static void main(String[] args) throws Exception {
        final SampleResource sampleResource = new SampleResource();
        sampleResource.hello();
        sampleResource.close(); // 자원반납시 꼭 닫아줘야한다.ㄹ
    }

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

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

resource를 사용한 후 반드시 close를 호출해서 리소스를 정리해야 한다.
(close 가 무조건 호출되도록 finally 블록을 이용한다.)

두 번째, try-finally를 이용한 자원 반납 방법

public class SampleResource implements AutoCloseable{
    public static void main(String[] args) throws Exception {
        SampleResource sampleResource = null;
        
        try {
            sampleResource = new SampleResource();
            sampleResource.hello();
        }finally {
            if(sampleResource != null){
                sampleResource.close();
            }
        }
    }
    
}

AutoCloseable을 구현하면, 명시적으로 close()를 호출하지 않아도 try블록이 끝날 때, close()를 호출한다.

finalizer와 cleaner 사용

첫 번째, 안전망 역할

  • AutoCloseable을 클라이언트에서 구현하지 않았을 경우를 대비한 안전망 역할

  • 클라이언트가 close()를 호출하지 않았을 경우 안하는것보다 늦게라도 해주는 것이 좋다. 물론 finalizercleaner가 호출될지 안될지 모르기는 하지만 말 그대로 안전망 삼아서 작성하는 것이 안하는것보다 좋기 때문이다.

  • 자바 라이브러리의 일부 클래스는 이를 위한 안전망 역할로 finalizer 를 제공한다. 대표적으로 FileInputStream, FileOutputStream, ThreadPoolExecutor 같은 finalizer가 있다.

[안전망으로 finalize 사용]

public class SafetyNet implements AutoCloseable {
    private boolean close;

    @Override
    public void close() throws Exception {
        if (this.close) {
            throw new IllegalStateException();
        }
        
        close = true;
        System.out.println("close");
    }

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

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        if (!this.close) {
            clone();
        }
    }
}
// 클라이언트가 close 하지 않았을수도 있으므로 close()를 재호출

두 번째, 네이티브 피어와 연결된 객체의 정리

  • 네이티브 피어 는 일반적인 자바 객체가 아니라 가비지 컬렉터는 그 존재를 알지 못하고 그 결과 가비지 컬렉터의 회수 대상이 되지 못한다. 즉시 회수해야 한다면 close() 를 사용해야 한다.

  • cleanerfilanlizer 가 처리하기 적당한 작업이다. 단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당된다. 이에 해당하지 않을 시 close()를 사용해야 한다.

  • 자바 클래스 -> 네이티브 메서드 호출 -> 네이트비 객체(네이티브 Peer) 순서로 작업한다.

[clearner를 안전망으로 사용한 경우 - 자바9이상 실행 가능]

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

    // 방의 상태. cleanable 과 공유한다.
    private final CleanRunner cleanRunner;

    // cleanable 객체. 수거 대상이 되면 방을 청소한다.
    private final Cleaner.Cleanable cleanable;

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

    // Cleanable 은 별도의 스레드로 clean 한다.
    // Room 을 참조하면 순환참조가 된다. 서로를 계속 참조하기 때문에 gc 에 의해 수거되지 않는다.
    // 따라서, State 인스턴스는 Room 을 참조해서는 절대 안 된다!
    private static class CleanRunner implements Runnable {
        int numJunkPiles; // 방안의 쓰레기 숫자 (clean 할 대상)

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

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

    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}
  • Cleaner 쓰레드(CleanRunner)는 정리할 대상인 인스턴스(Room)을 참조하면 안된다. 참조할 경우 서로를 계속해서 참조하기 때문에 순환 참조가 생겨서 GC 대상이 되지 못한다.

  • Cleaner 쓰레드(CleanRunner)는 반드시 static 클래스여야 한다. non-static클래스(익명 클래스도 마찬가지로)의 인스턴스는 그걸 감싸고있는 클래스의 인스턴스를 참조하기 때문이다.

  • Statecleaner가 방을 청소할 때 수거할 자원들을 담고있다. 이 cleanable 객체는 Room 생성자에서 cleanerRoomState를 등록할 때 얻는다. run()가 호출되는 상황은 두 가지다.

    • 보통은 Roomclose()를 호출할 때이다. close()에서 Cleanableclean()을 호출하면서 이 메서드 안에서 run()을 호출한다.

    • 다른 방법은 가비지 컬렉터가 Room을 회수할 때까지 클라이언트가 close()를 호출하지 않는다면, cleanerStaterun()을 호출한다.

[잘 작성된 클라이언트 코드]

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

클라이언트 코드가 모든 Room 생성을 try-with-resources 블록으로 감쌌다면 자동 청소는 필요하지 않다.

[방 청소를 하지 않는 클라이언트 코드]

public class Adult {
    public static void main(String[] args) throws Exception {
        new Room(99);
        System.out.println("안해");
    }
}
// 결과 : 안해

위 코드에서는 "방 청소" 가 출력되지 않는다. 이런 경우가 바로 앞에서 얘기한 예측할 수 없는 상황이다. 방 청소가 출력되는 것이 보장되지 않는다.

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

[Reference]
이펙티브자바 아이템8 finalizer와 cleaner 사용을 피해라
effectiveJava - avoid finalizer and cleaner
춤추는 개발자 - 이펙티브 자바 아이템8

0개의 댓글