Skip to content

Latest commit

 

History

History
112 lines (77 loc) · 5.53 KB

File metadata and controls

112 lines (77 loc) · 5.53 KB

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

작성자: 김대희

작성일: 2022년 4월 16일


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

finalizer

  • 예측할 수 없고 상황에 따라 위험할 수 있어 일반적으로 불필요하다.
  • 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.
  • 따라서 기본적으로 쓰지 말아야한다.

cleaner

  • finalizer의 대안으로 소개되지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

C++ 프로그래머라면 주의해야할 내용

finalizer와 cleaner는 C++의 destructor와 다른 개념이며, destructor는 constructor의 대척점이다.

하지만, 자바는 접근할 수 없는 객체를 회수하는 역할은 가비지 컬렉터가 담당한다.

finalizer와 cleaner 사용을 지양해야하는 이유

  • 즉시 수행된다는 보장이 없다. 즉, finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다. → 파일 닫기 같은 작업을 맡기면 중대한 오류를 일으킬 수 있다.
  • 수행 시점 뿐만이 아니라 수행 여부 조차 보장하지 않는다. → 꼭 반납해야하는 자원을 반납하지 못한 채 프로그램이 중단될 수도 있다.
💡 상태를 영구적으로 수정하는 작업에서 절대 finalizer나 cleaner에 의존해서는 안된다.
  • finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.
  • 또한, 심각한 성능 문제 또한 동반한다. try-with-resources 와 비교하여 약 50배나 느릴 수 있다.
  • finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다.
    • final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.

finalizer나 cleaner를 대신해 사용해야할 방법

  • AutoCloseable 을 구현해주고. 클라이언트에서 인스턴스를 다 쓰고 난다면 close 메서드를 호출한다. (예외가 발생해도 제대로 종료되도록 try-with-resources를 사용해야 한다)
  • 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다. → close 메서드에서 이 객체가 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 닫힌 후에 불렸다면 IllegalStateException을 던지도록 함.

그럼에도, finalizer와 cleaner를 사용하는 용도

  • 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할로, 클라이언트가 하지 않은 자원 회수를 늦게라도 해주는 것이 낫다는 의미.

  • 네이티브 피어(native peer)와 연결된 객체

    네이티브 객체란?

    • 네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체
    • Java 외의 프로그래밍 언어(C/C++ 이나 어셈블리)로 컴파일한 프로그램을 지칭하며, 이를 라이브러리로써 자바 피어가 실행할 수 있게 해주는 인터페이스를 JNI (Java Native Interface)라고 한다.

    java-squid/effective-java#8

    • 자바 객체가 아니니 가비지 컬렉터는 그 존재를 알지 못한다. 그 결과 가비지 컬렉터가 네이티브 객체는 회수하지 못한다.
    • 이런 경우 cleaner나 finalizer를 사용하며, 성능 저하를 감당할 수 있고, 심각한 자원을 가지고 있지 않을 경우 해당된다. → 즉시 회수해야 한다면 close 메서드를 사용해야한다.

cleaner를 안전망으로 사용하는 AutoCloseable 클래스

class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();
	
    // 청소가 필요한 자원으로 Room을 참조하게되면 순한 참조가 일어나기에 회수하지 못한다.
    // 따라서 절대 Room을 참조하지 말 것!
    private static class State implements Runnable {
        int numJunkPile;
	
        public State(int numJunkPile) {
            this.numJunkPile = numJunkPile;
        }
	
        @Override
        public void run() {
            System.out.println("방 청소");
            numJunkPile = 0;
        }
    }
	
    private final State state;
	
    private final Cleaner.Cleanable cleanable;
	
    public Room(int numJunkPile) {
        this.state = new State(numJunkPile);
        cleanable = cleaner.register(this, state);
    }
	
    @Override
    public void close() throws Exception {
        cleanable.clean();
    }
}
  • State 인스턴스는 절대로 Room 인스턴스를 참조해서는 안된다 → 순환 참조 발생으로 가비지 컬렉터가 회수 할 수 없게 됨.
  • 이는 State가 정적 중첩 클래스인 이유로, 정적이 아닌 클래스는 자동으로 바깥 객체의 참조를 갖게된다.
  • 비슷하게 람다 역시 바깥 객체의 참조를 갖기 쉬우니 사용하지 않는 것이 좋다.

Cleaner의 동작

cleaner는 Java 9 에서 finalizer의 대안으로 도입되었으며, cleaner객체에 모니터링할 object와, 객체가 소멸할 때 자원을 회수하는 action을 등록하고, clean() 메서드가 수행되면 action을 실행하는 방식으로 동작한다.

Untitled

Cleaner (Java SE 9 & JDK 9 )