8. finalizer와 cleaner 사용을 피하라

📝 아이템 8: finalizer와 cleaner 사용을 피하라

🔹 요약

finalizercleaner를 유용하게 사용할 일은 극히 드물다

  • 안전망 역할로 자원을 반납하고자 하는 경우

  • 네이티브 자원를 정리해야 하는 경우

finalizercleaner는 수행을 보장할 수 없다

  • 전적으로 GC(가비지 컬렉터) 알고리즘에 달려있으며 GC마다 다르다

  • 테스트한 JVM에선 완벽하게 동작하여도 고객의 시스템에선 다르게 동작할 가능성이 매우 높다

finalizercleaner는 우선 순위가 낮다

  • Finalizer 쓰레드는 우선순위가 낮아서 실행될 기회를 얻지 못 할 수도 있다

  • 처리되지 못한 객체가 쌓일 경우 OutOfMemoryError가 발생될 수 있다


📚 추가 개념

💡 vs C++ destructor

  • C++의 Destructor는 객체가 소멸될 때 호출되며, 객체와 관련된 자원을 반납하는 데 사용됩니다. C++에서는 객체가 스코프를 벗어나거나 delete로 메모리 할당을 해제할 때 자동으로 호출됩니다.

  • Java에서는 try-with-resources 구문이나 try-finally 블록이 자원을 관리하는 역할을 합니다. AutoCloseable 인터페이스를 구현한 클래스의 경우, try-with-resources 구문을 사용하여 자원을 자동으로 닫을 수 있습니다. 자원을 해제하려면 명시적으로 close() 메서드를 호출하거나, try-with-resources 블록을 사용하여 자동으로 호출되도록 합니다.

  • C++: Destructor는 객체 소멸 시 자원을 반납하는 역할을 수행.

  • Java: AutoCloseable 인터페이스와 try-with-resources를 통해 명시적 자원 관리가 이루어진다.

💡 네이티브 피어(Native Peer)란?

  • 네이티브 피어(Native Peer)Java 객체 가 아니라, 네이티브 메소드를 통해 Java 객체와 연결된 네이티브 객체 입니다. 즉, Java Application 에서 C, C++ 와 같은 네이티브 언어로 작성된 코드와 상호작용하기 위해 사용하는 객체입니다. 이 객체는 Java 가비지 컬렉터(GC) 의 추적을 받지 않기 때문에, Java 힙 에서 관리되지 않고, 자동으로 메모리 관리가 이루어지지 않습니다.

  • 네이티브 메소드Java 에서 C, C++ 로 작성된 네이티브 라이브러리의 기능을 호출하기 위한 메소드입니다. Java에서는 native 키워드를 사용하여 선언하고, 이를 통해 네이티브 코드로 정의된 메소소드를 호출할 수 있습니다. 이 메소드는 **JNI(Java Native Interface)**를 통해 연결됩니다.

  • **GC(가비지 컬렉터)**는 Java 힙에서 관리되는 객체만 추적하고, 네이티브 피어와 같은 네이티브 객체 는 관리하지 않습니다. 따라서 네이티브 피어는 자동으로 자원 해제를 하지 않으므로, **finalizer**나 close() 메서드를 사용하여 명시적으로 자원을 해제해야 합니다. 이때 finalizer는 Java 객체가 소멸될 때 자원을 해제하는 방법으로 사용됩니다.

  • 네이티브 메소드 finalizer 구문 예시:

    public class FinalizeExample {
      // 자원 해제 메소드드
      private void releaseResource() {
          System.out.println("자원 해제");
      }
    
      // finalize 메서드에서 자원 해제
      @Override
      protected void finalize() throws Throwable {
          try {
              releaseResource(); // 자원 해제
          } finally {
              super.finalize(); // 부모 클래스(Object) finalize 호출
          }
      }
    
      public static void main(String[] args) {
          FinalizeExample example = new FinalizeExample();
          example = null;  // 객체를 null로 설정하여 가비지 컬렉터가 finalize를 호출하도록 함
          System.gc();  // 강제로 가비지 컬렉터 실행 -> finalize 동작
      }
    }

💡 AutoCloseable이란?

  • AutoCloseable 은 자원을 자동으로 닫을 수 있는 기능을 제공하는 인터페이스입니다. 이 인터페이스를 구현한 객체는 try-with-resources 구문에서 사용되어, 자원을 자동으로 해제할 수 있습니다.

  • AutoCloseable 인터페이스는 close() 메서드를 정의하고 있습니다. 이 메서드는 자원을 닫는 데 사용되며, 파일, 네트워크 연결, 데이터베이스 연결 등과 같은 자원 관리에 활용됩니다.

  • AutoCloseable을 구현하면, 객체가 더 이상 필요하지 않을 때 명시적으로 close() 를 호출하지 않아도 됩니다. try-with-resources 구문을 사용하면, 블록을 벗어날 때 자동으로 close() 메서드가 호출되어 자원을 정리합니다.

  • 주요 사용 예시:

    • 파일 입출력 (예: FileInputStream, BufferedReader)

    • 데이터베이스 연결 (예: Connection, Statement)

    • 사용자 입력 (예: Scanner)

  • try-with-resources 구문 예시:

    try (BufferedReader br = new BufferedReader(new FileReader("example.txt"))) {
        // 파일 읽기 작업
    } catch (IOException e) {
        e.printStackTrace();
    }
    // try-with-resources 블록을 벗어나면 자동으로 close() 호출

🎯 중요한 점

📌 finalizer는 예측이 불가능하고, 위험하며, 불필요하다 📌 별도의 쓰레드를 사용하는 cleanerfinalizer보다 덜 위험하지만 여전히 예측 불가능하고, 느리고, 불필요하다 📌 AutoCloseable을 구현하여 자원 사용 후 명시적으로 close() 메소드를 호출하거나, try-with-resources 구문을 통해 자원을 자동으로 관리하자.


💡 코드 예제 및 설명

❌ 잘못된 예제 (finalizer를 사용한 경우)

public class SampleRunner {
    public static void main(String[] args) {
        SampleRunner runner = new SampleRunner();
        runner.run(); //  run() 메소드를 호출하여 FinalizerExample 객체를 생성하고 사용
        Thread.sleep(1000); // 1초후 "Clean up" 출력될 지 아무도 모른다.

        // run() 메서드가 종료되면 finalizerExample 객체는 더 이상 참조되지 않아 GC의 대상이 됨
        // 그러나 GC와 finalize() 메서드 호출 시점은 JVM에 의해 결정되므로 실행이 보장되지 않음
    }

    private void run() {
        FinalizerExample finalizerExample = new FinalizerExample();
        finalizerExample.hello();

        // 이 메서드가 종료되면 finalizerExample 변수는 스코프를 벗어나 참조가 끊어짐
        // 이제 이 객체는 GC의 대상이 됨
    }
}

public class FinalizerExample {

    @Override
    protected void finalize() throws Throwable {
        System.out.println("Clean up");
    }

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

✅ 개선된 예제 (AutoCloseable를 사용한 경우)

public static void main(String[] args) {
    SampleResource sampleResource = null;
    try {
        sampleResource = new SampleResource();
        sampleResource.hello();
    } finally {
        // finally 블록은 예외 발생 여부와 관계없이 항상 실행됨
        if (sampleResource != null) {
            sampleResource.close();
        }
    }
    // try-with-resources 구문이 도입되기 전에 주로 사용되던 방식
}

// AutoCloseable 인터페이스를 구현한 클래스
public class SampleResource implements AutoCloseable {

    @Override
    public void close() throws RuntimeException {
        // 자원을 해제하는 메소드
        System.out.println("close");
    }

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

✅ 개선된 예제2 (AutoCloseabletry-with-resources를 함께 사용한 경우)

public static void main(String[] args) {

    // 블록이 종료되면 자동으로 close() 메서드가 호출됨
    // 예외가 발생하더라도 close()는 반드시 호출됨
    try(SampleResource sampleResource = new SampleResource()) {
        sampleResource.hello();
    }
}

// AutoCloseable 인터페이스를 구현한 클래스
public class SampleResource implements AutoCloseable {

    @Override
    public void close() throws RuntimeException {
        // 자원을 해제하는 메소드
        System.out.println("close");
    }

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

❗ 어려웠던 점

⚠️ AutoCloseable을 처음 접해봤다. 그런데 나도 모르는 사이에 AutoCloseable이 구현된 클래스를 사용하고 있었다.

⚠️ Java 경험이 모던하지 않아서 cleanerfinalizer라는 걸 처음 접해봤다.

⚠️ 네이티브 메소드 개념을 들어는 봤는데 특성을 몰랐다.


💭 느낀 점

💡 finalizer와 cleaner의 존재를 모르고 사용해왔는데, GC(가비지 컬렉터)가 언제 객체를 정리할지는 예측할 수 있다는 건 알았지만, 이렇게 불확실할 줄은 몰랐다.

💡 네이티브 피어처럼 GC(가비지 컬렉터)가 직접 관리하지 않는 자원도 존재하므로, 자원 해제를 반드시 고려해야 한다.

💡 자바에서도 메모리 관리의 책임은 개발자에게 있다!

Last updated