본문 바로가기
Java

Item8 finalizer와 cleaner 사용을 피하라

by wwns 2023. 1. 15.
반응형

자바는 두 가지 객체 소멸자를 제공

  • `finalizer`
    • finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요
    • 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 함
    • 자바 9에서는 finalizer를 deprecated API로 지정
    • 기본적으로 `쓰지 말아야`한다
  • `cleaner`
    • finalizer보다 덜 위험하다
    • 예측할 수 없다 (언제 실행되는지)
    • 느리고, 일반적으로 불필요

`finalizer와 cleaner의 문제점

  1. 불확실성 (수행 보장 X)
    • 자바에서는 접근할 수 없게 된 객체를 회수하는 역할을 GC가 담당
      • finalizer나 cleaner를 수행할지는 적적으로 `GC 알고리즘`에 달렸고, 구현마다 천차만별
    • 객체에 접근할 수 없게 된 후 finalizer나 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없다
      • 자원회수가 지연되어 `OutOfMemoryError`와 같은 문제가 발생하기도 함
    • 자바 언어 명세에는 finalizer나 cleaner의 수행 시점뿐만 아니라 수행 여부조차 보장 X
    • finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다
      • 프로그램 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다.
  2. 예외 발생 시, 예외를 무시 후 바로 종료
    • finalizer 동작 중 발생한 예외는 무시되고, 처리할 작업이 남았더라도 그 순간 종료
      • 잡지 못한 예외 때문에 해당 객체는 자칫 마무리가 덜 된 상태로 남을 수 있음
      • 훼손된 객체는 어떻게 동작할지 예측할 수 없음
      • 보통의 경우엔 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하겠지만, finalizer에서는 경고조차 출력되지 않음
    • cleaner를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지는 않음.
  3. 성능 문제
    • finalizer와 cleaner를 사용하여 객체를 생성하고 파괴하는 것은 심각한 성능 문제를 동반
      • AutoCloseable 객체를 생성하고 GC가 수거하기까지 12ns가 걸림(try-with-resources로 자신을 닫음)
      • finalizer, cleaner를 사용하면 550ns, 500ns정도 걸림
      • 안전망 방식을 사용하면 약 66ns가 걸림
        • 안전망을 설치하는 대가로 성능이 약 5배 정도 느려진다는 뜻
  4. 보안 문제
    • finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제가 일어날 수 있음
      • 생성자나 직렬화 과정(readObject와 readResolve 메서드)에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer가 수행될 수 있게 됨
        • 이 finalizer는 정적 필드에 자신의 참조를 할당하여 GC가 처리하지 못하도록 할 수 있음
        • 이런 일그러진 객체가 만들어지면, 이 객체의 메서드를 호출해 허용되지 않는 작업을 수행하게 할 수 있음
      • 객체 생성을 막으려면 생성자에서 예외를 던지면 되지만 finalizer가 있다면 그렇지 않다.
        • final 클래스들은 그 누구도 하위 클래스를 만들 수 없으니 이 공격에서 안전
        • final이 아닌 클래스의 경우 아무 일도 하지 않는 finalizer 메서드를 만들고 final로 선언하자

AutoCloaseable을 구현해 주고 finalizer나 cleaner를 대신하여 사용하자

  • 클라이언트에서 인스턴스를 종료할 때 `close`메서드를 호출
    • 일반적으로 예외가 발생해도 제대로 종료되도록 `try-with-resources`를 사용해야 한다
  • 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다
    • `close` 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록
    • 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렀다면 IllegalStateException을 던짐

finalizer와 cleaner의 쓰임새

  • 자원의 소유자가 `close`메서드를 호출하지 않는 것에 대비한 안전망 역할
    • cleaner나 finalizer가 즉시 호출되리라는 보장은 없지만, 클라잉너트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안 하는 것보다는 낫다
  • 네이티브 피어(native peer)와 연결된 객체에서의 사용
 네이티브 피어란, 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말한다.

네이티브 메서드(Native Method)는 C, C++와 같은 네이티브 프로그래밍 언어로 작성한 메서드를 말한다. 
그리고 자바 프로그램에서 네이티브 메서드를 호출하는 기술을 JNI(Java Native Interface)라고 한다.
    • 네이티브 피어는 자바 객체가 아니기 때문에 GC가 처리하지 못함
    • 자바 피어를 회수할 때 네이티브 객체까지는 회수하지 못함
  • cleaner나 finalizer가 나서서 처리하기에 적당한 작업
    • 성능 저하와 즉시 회수되지 않아도 될 때에만 사용
    • 그렇지 않다면 `close`메서드를 사용해야 한다

cleaner를 안전망으로 활용하는 AutoCloseable 클래스 예시

public class Room implements AutoCloseable {
	
    private static final Cleaner cleaner = Cleaner.create();
    
    // 청소가 필요한 자원.
    // 정적 중첩 클래스. 절대 Room을 참조해서는 안 된다! (순환참조)
    private static class State implements Runnable {
    	int numJunkPiles;
        
        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 클래스

  • 정적 중첩 클래스
    • 자동으로 바깥 객체의 참조를 갖게 됨(GC의 처리를 받을 수 있음)
  • cleaner가 방을 청소할 때 수거할 자원을 담고 있음
  • Runnable을 구현하고, 그 안의 run 메서드는 cleanable에 의해 딱 한 번만 호출될 것
  • State 인스턴스는 절대로 Room 인스턴스를 참조하면 안 된다
    • 순환참조에 의해 GC가 처리하지 못하게 됨

위의 클래스를 사용하는 클라이언트 코드 예시

public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("안녕~");
        }
    }
}
  • 앞서 말한 대로 cleaner는 단지 안전망으로만 쓰임
  • Room의 생성을 `try-with-resources` 블록으로 감쌌다면 자동 청소(GC)는 전혀 필요하지 않다

위 프로그램은 "안녕~"을 출력하고 나서 "방 청소"를 출력할 것

 

`try-with-resources` 블록으로 감싸지 않은 코드 예시

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

"아무렴"에 이어 "방 청소"가 출력되리라 기대하겠지만, "방 청소"는 출력할 수도, 출력하지 않을 수도 있다.

앞에 말한 `예측할 수 없는` 상황이다


정리

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

 

`try-with-resources`를 사용해서 자원을 회수해 주는 게 제일 베스트인 것 같고, cleaner의 사용법을 잘 알고 사용해야 한다는 것을 전하는 것 같다.

반응형