본문 바로가기
JAVA/Effective Java

item 8. finalizer과 cleaner 사용을 피하라.

by Garonguri 2022. 4. 16.
728x90

Finalizer

- Object에 존재하는 finalize()를 의미한다.

- 객체 소멸자이다.

- 클래스 객체가 더 이상 사용되지 않으면 GC가 자동으로 호출된다.

- 예측 불가하고 느리다.

 

[ 사용 ]

- 안전망 역할로 자원을 반납하고자 하는 경우에 사용된다.

- 네이티브 피어와 연결된 객체에서 네이티브 리소스를 정리해야 하는 경우에 사용한다.

 

 

Cleaner

- 객체 소멸자이다.

- Java9에서 finalizer가 deprecated 됐고, cleaner이 새로 생겼다.

- 별도의 thread를 사용해 finalizer보다 덜 위험하지만, 예측 불가하고 느리다.

 

finalizer과 cleaner는 객체 소멸자지만 c++의 파괴자와는 다르다.

c++에서의 destructor 역할 1 : 특정 객체와 관련된 자원을 회수하는 역할. -> java에서는 GC가 담당한다.

c++에서의 destructor 역할 2 : 비메모리 자원을 회수한다. -> java에서는 try-with-resources, try-finally를 통해 해결한다.

 

단점 1. 언제 실행될지 알 수 없다.

- finalizer과 cleaner는 즉시 수행된다는 보장이 없다.

- 객체가 필요 없어진 시점부터, 두 객체 소멸자가 실행되는 시점까지 얼마나 소요될지 알 수 없다.

- 타이밍이 중요한 작업은 절대로 finalizer, cleaner에게 맡기면 안된다.

단점 2. 인스턴스 반납 지연

- finalizer thread는 다른 어플리케이션 thread보다 우선 순위가 낮아서, 언제 실행될지 알 수 없다.

- cleaner은 자신을 수행할 thread를 제어할 수 있지만, 여전히 백그라운드에서 수행되며 GC의 통제하게 있기 때문에, 즉시 수행되리라는 보장이 없다.

단점 3. 아예 실행되지 않을 가능성 존재

- 심지어, 반드시 실행된다는 보장도 없다.

- System.gc(), System.runFinalization() : 실행될 가능성은 높여줌. 보장해주지는 않는다.

- System.runFinalizersOnExit() : 실행 보장을 지원하긴 하지만, 치명적 결함이 존재한다. (수십년간 Deprecated 상태임)

단점 4. 예외 발생 무시

- finalizer을 실행하는 동안에는, catch되지 않는 예외가 무시되며 finalize가 끝난다.

- 이로 인해 객체가 망가질 가능성이 있고, 다른 스레드에서 이 객체에 접근하면 예측할 수 없는 문제가 생긴다.

- cleaner의 경우네는 자신이 동작할 스레드를 통제하므로 4의 문제는 발생하지 않는다.

단점 5. 성능 문제

- AutoClosable 객체를 만들고 try-with-resource로 자원을 반납하면 : 12ns 소요

- finalizer을 사용해 객체를 만들고 파괴한 경우 : 550ns 소요

- cleaner을 사용해 객체를 만들고 파괴한 경우 : 500ns 소요

( 안전망 상태로만 사용하면 빨라지긴 한다. 그래도 66ns 소요)

단점 6. 보안 문제

- finalizer 공격에 노출되어 심각한 보안 이슈에 이용될 수 있다.

 [공격 과정]

-> 어떤 클래스가 있고, 그 클래스를 공격하려는 클래스가 해당 클래스를 상속받는다.

-> 공격하려는 클래스의 인스턴스를 생성하거나 직렬화하는 도중 예외가 발생하면, 원래는 죽었어야 할 객체의 finalizer가 수행될 수 있다. -> 해당 인스턴스의 레퍼런스를 기록할수도 있고, 그러면 하위 클래스는 GC의 대상이 되지 못할 수도 있다. 

- 생성자에서 예외가 발생해 인스턴스가 존재하지 않아야 하는데, finalizer때문에 살아남아 생기는 문제이다.

 

- 이런 경우 final 클래스는 상속이 안되기 때문에, finalize()메서드를 final로 선언해 예방할 수 있다.

 

 

그럼 뭘 사용해서 자원을 반납해야 하는가?

  AutoCloseable  

- 자원 반납이 필요한 클래스에서 AutoCloseable 인터페이스를 구현한다.

- 그 후 try-catch-resource를 사용하거나, client가 인스턴스를 사용하고 나면 close()를 명시적으로 호출하면 된다.

- close() 메소드는 현재 인스턴스 상태가 이미 종료됐는지 확인하고, 이미 반납이 끝났는데 호출되면 IllegalStateException을 던진다.

- 예외가 발생해도 제대로 종료할 수 있게 해주는 try-with-resource를 사용하는 것이 좋다.

public class AutoCloseableExample {
    public static void main(String[] args) {
        try (CustomResource customResource = new CustomResource()){
            customResource.doSomething();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

public class CustomResource implements AutoCloseable {

    public void doSomething(){
        System.out.println("Do something ...");
    }

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

참고 : close 메서드를 구현할 때는 구체적인 exception을 throw 하고,

close 동작이 전혀 실패할 리가 없을 때는 exception을 던지지 않도록 구현하는 것을 강력히 권고하고 있다고 한다.

특히 close 메서드에서 InterruptedException을 던지지 않는 것을 강하게 권고한다.

InterruptedException은 쓰레드의 인터럽트 상태와 상호작용 하므로 interruptedException이 억제되었을 때 런타임에서 잘못된 동작이 발생할 수 있기 때문이라고 한다.

728x90

댓글