본문 바로가기
개발 공부/Java

이펙티브 자바 아이템 8 - finalizer와 cleaner 사용을 피하라 - 핵심 정리

by 개발인생 2022. 10. 12.
반응형

아이템 8 - finalizer와 cleaner 사용을 피하라 - 핵심 정리

이 글은 백기선 님의 이펙티브 자바 강의와 이펙티브 자바 3 / E 편을 참고하여 작성하였습니다.

finalizer 와 cleaner 사용을 피해야한다.

cleaner 는 자바9 버전부터 새로 들어온 기능이다.

finalizer 와 cleaner 는 같인 일을 하는 기능, 자원을 반납하는 기능이다.

하지만 finalizer 와 cleaner 는 즉시 수행한다는 보장이 없다.

만약 객체가 가지고있던 일부 리소스를 제대로 정리하지 않고 객체를 소멸시키면 어떻게될까?

운영체제에서는 파일 핸들러와 운영체제마다 오픈할 수 있는 파일 갯수가 제한되어있다.

소켓 갯수 또한 제한이 있다.

소켓이나 파일을 만들때마다 파일 핸들러가 만들어진다.

무한정 파일 핸들러를 만들 수가 없다.

즉, 파일들이 많이 열려있으면 더이상 파일을 열 수 없다는 에러가 발생한다.

이 근본적인 원인은 자원반납이 제대로 처리되지 않았기 때문이다.

그래서 객체를 소멸할 때 리소스를 적절한 타이밍에 정리를 하려고 만들어진게 finalizer 와 cleaner 다.

하지만 사실상 finalizer 와 cleaner 는 사용할 수 없다

특히나 finalizer 사용은 위험하다.

또한 finalizer 와 cleaner 는 실행 자체가 안될 수도 있다.

finalizer 는 동작 중 예외가 발생하면 자원 정리 작업이 처리되지 않을수도 있다.


finalizer

public class FinalizerIsBad {

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

finalizer 는 finalize 메서드를 오바라이딩하면 된다.

    @Deprecated(since="9")
    protected void finalize() throws Throwable { }

finalize 는 자바9 버전부터 사용을 자제하라고 권장하고 있다.

public class App {
   /**
    * 코드 참고 https://www.baeldung.com/java-finalize
    */
   public static void main(String[] args) throws InterruptedException, ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
      int i = 0;
      while(true) {
         i++;
         new FinalizerIsBad();

         if ((i % 1_000_000) == 0) {
            Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
            Field queueStaticField = finalizerClass.getDeclaredField("queue");
            queueStaticField.setAccessible(true);
            ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null);

            Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength");
            queueLengthField.setAccessible(true);
            long queueLength = (long) queueLengthField.get(referenceQueue);
            System.out.format("There are %d references in the queue%n", queueLength);
         }
      }
   }
}

가비지 컬렉션 대상이되면 Finalizer 안에 있는 레퍼런스 큐에 들어가서 finalize() 메서드를 실행하게 된다.

위의 코드는 Finalizer 안에 있는 레퍼런스 큐에 얼마나 객체가 쌓여있는지에 대한 코드이다.

객체를 만드느라 바빠서 Finalizer 안에 있는 레퍼런스 큐를 정리하지 못해

Finalizer 안에 있는 레퍼런스 큐의 사이즈가 크게 출력되는 경우도 있다.

Finalizer 안에 있는 레퍼런스 큐를 정리하는 스레드의 우선순위가 낮기 때문이다.

public class FinalizerIsBad {

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

위의 finalize() 메서드에서 다른 오브젝트를 참조하거나, 자기 자신을 참조하게되면

사실상 finalize() 메서드 실행시 오브젝트가 늘어나게 된다.


cleaner

자바 9버전 부터는 cleaner 를 사용할 수 있다.

public class BigObject {
   private List<Object> resource;

   public BigObject(List<Object> resource) {
      this.resource = resource;
   }

   public static class ResourceCleaner implements Runnable {

      private List<Object> resourceToClean;

      public ResourceCleaner(List<Object> resourceToClean) {
         this.resourceToClean = resourceToClean;
      }

      @Override
      public void run() {
         resourceToClean = null;
         System.out.println("cleaned up.");
      }
   }
}

BigObject 클래스 안에 resource 필드가 있다.

resource 필드는 BigObject 클래스가 소멸될 때마다 정리되어야하는 리소스이다.

리소스 정리 작업을 Runnable 의 구현체로 구현한다.

주의할 점은 inner class 로 만들거라면 static 클래스로 정의한다.

또한 절대로 BigObject 클래스에 대한 레퍼런스가 있으면 안된다.

리소스 정리 작업을 Runnable 의 구현체에서 정리하려는 Object 를 참조하면 안된다.

가비지 컬렉션이 될때 리소스 정리 작업을 Runnable 의 구현체에서 정리하려는 Object 를 참조하게 되면

객체가 부활할 수 있다.

public class CleanerIsNotGood {
   public static void main(String[] args) throws InterruptedException {
      Cleaner cleaner = Cleaner.create(); // 클리너 생성

      List<Object> resourceToCleanUp = new ArrayList<>();
      BigObject bigObject = new BigObject(resourceToCleanUp); // 객체 생성

      // 클리너 등록
      cleaner.register(bigObject, new BigObject.ResourceCleaner(resourceToCleanUp));

      bigObject = null;
      System.gc();
      Thread.sleep(3000L);
   }
}

cleaner 사용은 팬텀 레퍼런스 사용과 비슷하다.

cleaner 는 팬텀 레퍼런스를 사용해 만들어졌기 때문이다.

위의 코드는 bigObject 가 GC의 대상이 될 때 resourceToCleanUp 의 자원을 해제하는 작업이다.

   public static class ResourceCleaner implements Runnable {

      private List<Object> resourceToClean;

      public ResourceCleaner(List<Object> resourceToClean) {
         this.resourceToClean = resourceToClean;
      }

      @Override
      public void run() {
         resourceToClean = null;
         System.out.println("cleaned up.");
      }
   }

정리하는 작업 자체는 BigObject 클래스에 정의한 Runnable 의 구현체이다.


AutoClosable

자원 정리 작업으로 권장하는 방식은 AutoClosable 사용 이다.

public class AutoClosableIsGood implements AutoCloseable {

   private BufferedReader reader;

   public AutoClosableIsGood(String path) {
      try {
         this.reader = new BufferedReader(new FileReader(path));
      } catch (FileNotFoundException e) {
         throw new IllegalArgumentException(path);
      }
   }

   @Override
   public void close() {
      try {
         reader.close();
      } catch (IOException e) {
         throw new RuntimeException(e);
      }
   }
}

AutoCloseable 을 구현한 클래스를 만든다.

close() 메서드를 재정의 한다.

close() 메서드 안에 자원 정리 작업을 하면된다.

public class App {

   public static void main(String[] args) {
      try(AutoClosableIsGood good = new AutoClosableIsGood("")) {
         // TODO 자원 반납 처리가 됨.

      }
   }
}

클라이언트 코드에서 try-with-resource 를 사용하여 객체를 사용한다.


그렇다면 cleaner 는 언제 사용하면 좋을까?

public class AutoClosableIsGood implements AutoCloseable {

   private BufferedReader reader;

   public AutoClosableIsGood(String path) {
      try {
         this.reader = new BufferedReader(new FileReader(path));
      } catch (FileNotFoundException e) {
         throw new IllegalArgumentException(path);
      }
   }

   @Override
   public void close() {
      try {
         reader.close();
      } catch (IOException e) {
         throw new RuntimeException(e);
      }
   }
}

AutoClosableIsGood 를 작성하면서 모든 클라이언트들이 try-with-resource 를 사용하길 원하지만

그렇게 안쓰는 경우가 있을 수 있다.

그래서 try-with-resource 를 사용하지 않더라도

GC 를 할 때 자원이 반납되는 기회를 가질 수 있도록 안전망으로 cleaner 를 사용한다.

하지만 이렇게 cleaner 를 사용한다 해도 즉시 호출되리라는 보장은 없다.

또한 cleaner 는 네이티브 피어 자원을 회수 할 때도 사용할 수 있다.

네이티브 피어 자원은 자바에 있는 모든 코드 중 일부는 네이티브 (OS 에 특화된 코드) 코드에 접근하게 된다.

네이티브 피어 에 접근하는 자바 피어 의 예로는 Jframe 이 있다.

cleaner 를 사용해 자바 피어 에서 접근한 네이티브 피어 자원을 정리할 기회를 줄 수 있다.

하지만, 성능에 심각한 이슈를 줄 수 있는 네이티브 피어

close 메서드나 AutoCloseable 구현 후 try-with-resource 를 사용하는 방법 등으로 직접 회수하는게 좋다.

public class Room implements AutoCloseable {
   private static final Cleaner cleaner = Cleaner.create();

   // 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다!
   private static class State implements Runnable {
      int numJunkPiles; // Number of junk piles in this room

      State(int numJunkPiles) {
         this.numJunkPiles = numJunkPiles;
      }

      // close 메서드나 cleaner가 호출한다.
      @Override public void run() {
         System.out.println("Cleaning room");
         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();
   }
}

Room 클래스를 작성한다.

// cleaner 안전망을 갖춘 자원을 제대로 활용하는 클라이언트 (45쪽)
public class Adult {
   public static void main(String[] args) {
      try (Room myRoom = new Room(7)) {
         System.out.println("안녕~");
      }
   }
}

Adult 클래스 에서는 try-with-resource 를 사용하고 있다.

자원 반납이 잘 되고 있다.

// cleaner 안전망을 갖춘 자원을 제대로 활용하지 못하는 클라이언트 (45쪽)
public class Teenager {

   public static void main(String[] args) {
      new Room(99);
      System.out.println("Peace out");

      // 다음 줄의 주석을 해제한 후 동작을 다시 확인해보자.
      // 단, 가비지 컬렉러를 강제로 호출하는 이런 방식에 의존해서는 절대 안 된다!
//      System.gc();
   }
}

Teenager 클래스 에서는 try-with-resource 를 사용하지 않고 Room 클래스를 사용한다.

이렇게 사용을 하면 자원 반납을 하지 않고 로직이 끝날 수 있다.

이러한 경우에 자원을 반납할 수 있는 기회를 cleaner 를 통해 줄 수 있다.

반응형

댓글