제육's 휘발성 코딩
article thumbnail
반응형

finalizer와 cleaner 사용을 피하라

자바는 finalizercleaner 두 가지로 객체 소멸자를 제공한다. 그 중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다. cleanerfinalizer 보다 덜 위험하지만, 여전히 예측할 수 없고, 느리며 일반적으로 불필요하다. 따라서 Java 9 에서 부터 deprecated(사용 자제) 되었으며, 그 대안으로 cleaner를 사용한다.

deprecated 관련 문서 : https://docs.oracle.com/javase/9/docs/api/java/lang/Object.html#finalize--

 

정리에 앞서 상태를 영구적으로 수정하는 작업에서는 절대로 두 가지의 객체 소멸자를 사용하면 안된다. 예를 들어 데이터베이스 같은 공유 자원의 영구 lock 해제를 두 가지 방식으로 사용하면 분산 시스템 전체가 멈출 수 있다. 또한 finalizer는 예외 발생 시 처리할 작업이 남았더라도 그 순간 종료된다. (스레드를 관리하지 않기 때문에 스택 추적도 불가능, finailzer 공격도 발생)

 

즉, finalizer 는 사용하면 안되며, 공격에 대비하기 위해 아무 작업도 하지 않는 finalizer를 상속받고, final로 선언해주는 것이 안전하다.

두 객체 소멸자를 대신할 방법으론 AutoCloseable을 구현하고 다 쓰고 나면 close 메서드를 호출하면 된다.

 

finalizer

finalizer는 예측할 수 없다는 의미에 대해 알아보자. 본 예제는 deprecated 되기 전인 자바 8 버전에서 수행했다.

public class FinalizeTest {
    public static void main(String[] args) {

        for(int i = 0; i < 20; i++) {
            Finalize test = new Finalize(i);
            test = null; // 참조 해제
            System.gc(); // GC 호출
        }
    }
}

class Finalize {
    private int num;

    public Finalize(int num) {
        this.num = num;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(num+ "번 finalize 실행");
    }
}

/*
출력 결과 - 비순차적이고, 실행이 보장되지 않는다.
4번 finalize 실행
0번 finalize 실행
13번 finalize 실행
16번 finalize 실행
17번 finalize 실행
18번 finalize 실행
19번 finalize 실행
15번 finalize 실행
14번 finalize 실행
11번 finalize 실행
12번 finalize 실행
10번 finalize 실행
*/
  • finalize() 를 상속 받은 후 실행 단에서 사용하는 객체를 참조 해제(null) 후, System.gc()로 GC를 실행 시켰다 (객체 소멸자가 실행될 가능성만 높여준다). 결과를 보면 GC가 처리하는 순서는 무작위로 제거되며, 전부 삭제시키는 것이 아닌 메모리의 용량에 따라 소멸 여부를 결정하게 된다.
  • finalizer 는 어떤 스레드가 수행할 지 명시하지 않기 때문에 수 많은 finalizer 스레드 들이 대기열에서 회수되기를 기다리는 문제(OutOfMemeryError)가 발생한다. 이 문제를 해결할 방법이 없어서 자바 9버전에서 cleaner 를 통해 자신을 수행할 스레드를 제어할 수 있는 기능을 추가했다. 단, 수행 순서는 여전히 보장할 수 없다.

 

cleaner

자바 9에서 대안으로 나온 cleaner는 어떨까? cleaner를 사용할지는 내부 구현 방식에서 선택의 문제이다. 즉, finailizer 처럼 Override 하는 것이 아닌 구성을 통해 cleaner를 사용해야 한다.

import java.lang.ref.Cleaner;

public class Room implements AutoCloseable {

  private static final Cleaner cleaner = Cleaner.create();

  // Room 인스턴스를 참조해서는 순환참조가 발생하므로 State는 static class 정의
  private static class State implements Runnable {
    int numJunkPiles;

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

    @Override
    public void run() {
      System.out.println("방 청소");
      numJunkPiles = 0;
    }
  }

  // 방의 상태. cleanable과 공유한다.
  private final State state;

  private final Cleaner.Cleanable cleanable;

  public Room(int numJunkPiles) {
    state = new State(numJunkPiles);
    cleanable = cleaner.register(this, state);
  }

  @Override
  public void close() {
    cleanable.clean();
  }
}
  • static으로 선언된 중첩 클래스인 State는 cleaner가 방을 청소할 때 수거할 자원들을 담고 있다.
    • StateRoom 인스턴스를 참조해서는 안된다. 그렇게 되면 순환 참조가 발생하여 GC가 회수해갈 기회를 주지 못한다. 따라서 반드시 State를 정적 중첩 클래스로 만들어야 한다.
    • 방 안의 쓰레기 수를 뜻하는 numJunkPiles 필드가 수거할 자원에 해당 된다. 이 필드는 더 현실적으로는 네이티브 피어를 가리키는 포인터를 담은 final long 변수여야 한다.
    • 네이티브 피어란? C / C++ 등의 어셈블리 프로그램을 컴파일한 기계어 프로그램을 지칭한다. 자바는 JNI (Java Native Interface) 라이브러리를 사용하여 자바 피어를 실행 시켜 준다. 자바 피어가 로딩될 때 System.loadLibrary() 메서드를 호출해서 네이티브 피어를 로딩하고 native 키워드를 사용하여 호출하는 방식으로 사용된다. JNI 는 직접 사용할일은 거의 없고 대부분 maven central 에서 자바 라이브러리를 가져와 사용한다.

 

/*
    안녕~ 출력 후 Room Clean 출력
*/
class Adult {
  public static void main(String[] args) {
    try (Room myRoom = new Room(7)) {
      System.out.println("안녕~");
    }
  }
}
  • 다음과 같이 try-with-resources 블록으로 Room 생성을 감싸서 처리하면 자동 청소는 전혀 필요하지 않게 된다.

 

class Adult {
  public static void main(String[] args) {
    new Room(99);
    System.out.println("아무렴");
  }
}
  • 하지만 다음의 경우에는 청소가 이루어지지 않는다. cleaner 동작이 System.exit() 을 호출 할때 실행될지는 보장하지 않는다.

finalizercleaner는 결국 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할이다. 하지만 즉시 호출되리라는 보장은 없다. 따라서 단순한 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자. 물론 해당 경우에도 불확실성과 성능 저하에 주의해야 한다.


REFERENCES

https://github.com/java-squid/effective-java/issues/8

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^