ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 객체 생성과 파괴 - 아이템 8. finalizer와 cleaner 사용을 피하라
    Study/Effective Java 2022. 12. 13. 11:09

    finalizer와 cleaner 사용을 피하라

    사용하지 말아야 하는 이유

    Java에서 제공하는 두 가지 객체 소멸자인 finalize와 cleaner는 기본적으로 쓰지 말아야 한다. 사용하지 말아야 하는 이유는 다음과 같다.

     

    1. finalizer : 예측할 수 없고, 위험하며, 느리고, 일반적으로 불필요하다.

    2. cleaner : finalizer보다는 덜 위험하지만 여전히 예측할 수 없고, 느리며, 보통을 불필요하다.

     

    좀 더 구체적으로 살펴보도록 하겠다.

     

    단점 1. 즉시 수행된다는 보장이 없다.

    finalizer와 cleaner는 즉시 수행된다는 보장이 없다. 즉, finalizer와 cleaner로는 제때 실행되어야 하는 작업을 절대 할 수 없다. finalizer와 cleaner의 수행 속도는 가비지 컬렉터에 달렸으며, 가비지 컬렉터 구현마다 다르다. 가비지 컬렉터는 GC가 될 때 finalizer()를 수행하는데, 문제는 이 메서드가 언제 호출될지 모른다는 것이다.

     

    단점 2. 자원 회수가 제멋대로 지연된다.

    finalizer 스레드는 다른 애플리케이션 스레드보다 우선순위가 낮기 때문에, 실행될 기회를 제대로 얻지 못할 수 있다. cleaner의 경우 자신을 수행할 스레드를 제어할 수 있어 조금 낫긴 하지만, 여전히 가비지 컬렉터의 통제하에 있기 때문에, 즉시 수행된다는 보장이 없다. 즉, 인스턴스의 반납을 지연시킬 수 있으며, 심지어는 실행이 되지 않을 수도 있다.

     

    단점 3. 수행 여부조차 보장하지 않는다.

    자바 언어 명세는 finalizer나 cleaner의 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다. 접근할 수 없는 일부 객체에 딸린 종료 작업을 전혀 수행하지 못한 채 프로그램이 중단될 수도 있다는 뜻이다. 따라서, 프로그램 생애주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다.

     

    단점 4. 동작 중 발생한 예외가 무시된다.

    finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남아 있더라도 예외가 발생한 순간 종료된다. 잡지 못한 예외 때문에 해당 객체는 마무리가 덜 된 상태로 남을 수 있다. 그리고 다른 스레드가 이처럼 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없다. 보통의 경우, 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하지만, 동일한 일이 finalizer에서 일어난다면 경고조차 출력하지 않는다. 그나마 cleaner를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에, 이러한 문제가 발생하지 않는다.

     

    단점 5. 심각한 성능 문제를 동반한다.

    finalizer와 cleaner는 가비지 컬렉터의 효율을 떨어트리기 때문에, 심각한 성능 문제를 야기한다.

     

    단점 6. 보안 문제를 일으킬 수도 있다.

    finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수 있다. finalizer 공격 원리는 다음과 같다. 생성자나 직렬화 과정에서 예외가 발생하면, 이 생성되다 만 객체에서 악의적인 하위 클래스의 finalizer 수행될 수 있다. 이 finalizer는 정적 필드에 자신의 참조를 할당하여 가비지 컬렉터가 수집하지 못하게 막을 수 있다. 이렇게, 잘못된 객체가 만들어지고 나면, 이 객체의 메서드를 호출해 애초에는 허용되지 않았을 작업을 수행할 수도 있다. 

    객체 생성을 막기 위해서는 생성자에서 예외를 던지는 것만으로 충분하지만, finalizer가 있다면 그렇지 않다. 만약, final이 아닌 클래스를 finalizer 공격으로부터 방어하기 위해서는 아무 일도 하지 않는 finalizer 메서드를 만들고 final로 선언함으로써 해결할 수 있다.

     

    AutoCloseable 구현을 통한 자원의 정상적인 반납

    파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 finalizer나 cleaner를 대신해 줄 방법은 바로 AutoCloseable을 사용하는 것이다. AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다.

     

    1. try-finally : 명시적인 자원 반납

    public class SamplerResource implements AutoCloseable {
        @Override
        public void close() throws RuntimeException {
            System.out.println("close");
        }
    
        public void hello() {
            System.out.println("hello");
        }
    }
    

    AutoCloseable 인터페이스를 구현한 SampleResource 클래스를 정의한다. 해당 클래스는 자원을 정리하는 close() 메서드를 재정의 하였다.

     

    public class Main {
        public static void main(String[] args) {
            SamplerResource resource = new SamplerResource();
    
            try {
                resource.hello();   // 리소스 사용
            } finally {
                resource.close();   // 리소스 정리
            }
        }
    }

    리소스를 사용한 쪽에서 사용 후 반드시 close() 메서드를 호출하여 리소스를 정리해줘야 한다. close() 메서드가 호출이 되도록 보장하기 위해 try-finally 블록을 사용하는 것이 좋다.

     

    2. try-with-resource : 암묵적인 자원 반납

    public class SamplerResource implements AutoCloseable {
        @Override
        public void close() throws RuntimeException {
            System.out.println("close");
        }
    
        public void hello() {
            System.out.println("hello");
        }
    }
    

    AutoCloseable 인터페이스를 구현한 SampleResource 클래스를 정의한다. 해당 클래스는 자원을 정리하는 close() 메서드를 재정의 하였다.

     

    public class Main {
        public static void main(String[] args) {
            try (SamplerResource resource = new SamplerResource()) {
                resource.hello();   // 리소스 사용
            }
        }
    }

    try-with-resource 블록의 경우, AutoCloseable 인터페이스를 구현하면 명시적으로 close() 메서드를 호출하지 않아도 try 블록이 종료될 때, close() 메서드를 호출한다.

     

    finalizer와 cleaner의 사용 시기

    finalizer와 cleaner의 적절한 쓰임새는 두 가지가 있다. 

     

    1. 자원의 소유자가 close() 메서드를 호출하지 않는 것에 대비한 안전망 역할

    finalizer나 cleaner가 즉시 호출될 것이라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안 하는 것보다 낫다. 이러한 상황에서 finalizer나 cleaner를 사용한다.

    자바 라이브러리 일부 클래스는 안전망 역할의 finalizer를 제공한다. FileInputStream, FileOutputStream, ThreadPoolExecutor가 대표적인 예이다.

     

    public class SamplerResource implements AutoCloseable {
    
        private boolean closed;
    
        @Override
        public void close() throws RuntimeException {
            if (this.closed) {
                throw new IllegalStateException();
            }
            closed = true;
            System.out.println("close");
        }
    
        public void hello() {
            System.out.println("hello");
        }
    
        @Override
        protected void finalize() throws Throwable {
            if (!this.closed) {
                close();   // 한 번 더 호출
            }
        }
    }

    클라이언트가 자원회수를 하지 않을 것을 대비하여 finalizer()  메서드 내에서 close() 메서드를 한 번 더 호출한다.

     

    2. 네이티브 피어와 연결된 객체를 회수하는 역할

    네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 의미한다. 네이티브 피어는 자바 객체가 아니기 때문에 가비지 컬렉터는 그 존재를 알지 못한다. 그 결과 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못한다. finalizer나 cleaner가 나서서 처리하기에 적당한 작업이다. 단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당된다. 성능 저하를 감당할 수 없거나 네이티브 피어가 사용하는 자원을 즉시 회수해야 하는 경우 앞서 설명한 close() 메서드를 사용해야 한다.

     

    최종 정리

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


    출처

     이펙티브 자바 Effective Java 3/E. 조슈아 블로크 저자(글) · 개앞맵시(이복연) 번역

    https://velog.io/@im_lily/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-%EC%95%84%EC%9D%B4%ED%85%9C-8

     

    728x90

    댓글

Designed by Tistory.