#07 Finalizer의 사용을 피하자
예전부터 Finalizer를 이용하지 말라는 내용을 종종 봤는데, 왜 그런지에 대해 알아보겠습니다.
그리고 어떤 경우에 이용하고, 이때 주의할 점에 대해서도 알아보겠습니다.
Finalizer 클래스에 정의하는 finalize() 메서드를 말합니다.
finalize() 메서드는 클래스의 객체가 더 이상 이용되자 않으면 JVM의 Garbage collector가 메모리에서 객체를 없애기 전에 자동으로 호출됩니다.
언뜻 보면 아무 문제가 없어 보이는데 몇 가지 문제점들이 있습니다.
객체를 이용할 수 없게 되는 시점부터 Finalizer가 실행되는 시점까지는 긴 시간이 소요될 수 있습니다.
이 Finalizer가 얼마나 빨리 실행되는지는 주로 GC 알고리즘에 달려있으며, 이 알고리즘은 JVM 종류에 따라 다양합니다.
그리고 어떤 스레드가 Finalizer를 실행할지 자바 언어 명세에는 보장되어 있지 않으므로, Finalizer 스레드가 실행되지 않아 다른 문제가 발생할 수 도 있습니다.
따라서 반드시 실행해야 하는 코드를 Finalizer에서 작성하면 안 됩니다.
System.gc와 System.runFinalization 메서드를 호출하더라도 Finalizer가 실행될 가능성을 높여줄 뿐 반드시 실행됨을 보장하지는 않습니다.
Finalizer를 보장하는 System.runFinalizersOnExit(boolean) 함수와 Runtime.runFinalizersOnExit(boolean) 함수가 있지만 치명적인 결함으로 인해 현재는 이용 금지되어 있습니다.
Finalize 하는 동안 catch 되지 않는 예외가 발생하면 그 예외는 무시되고 Finalize가 끝납니다.
일반적으로 catch 되지 않는 예외가 발생하면, 실행이 중단되고 stack trace가 출력됩니다.
하지만 Finalize 내에서 발생하면 예외가 무시되므로 불안정한 상태에서 예측할 수 없는 결과가 발생할 수 있습니다.
클래스의 객체를 생성하고 소멸시키는 시간이 Finalizer를 이용하면 많게는 수백 배 정도 차이가 납니다.
따라서 가급적 Finalizer를 통해 객체를 소멸시키지 않고 다른 방법을 써야 합니다.
아래와 같이 try-finally를 이용하면 객체 이용 중 예외가 발생해도 항상 실행됩니다.
// try-finally block을 이용하면 종료 메서드의 실행이 보장됩니다.
Student student = new Student(...);
try {
student.study(); // student 객체를 통한 처리
} catch(Exception e) {
// 예외 처리
} finally {
student.terminate(); // 종료 메서드 호출
}
위에서 발생되는 문제점들만 보면 Finalizer는 이용해서는 안됩니다.
그러면 어떤 경우에 이용하면 좋을지 알아보겠습니다.
객체의 종료 메서드 호출을 빠뜨렸을 경우에 "안정망" 역할을 하는 경우입니다.
즉, 클라이언트가 종료 메서드 호출에 실패하는 경우를 대비해서 이용할 수 있습니다.
물론 늦게 Finalizer가 늦게 호출되거나 심지어 호출이 안될 수 도 있지만, 이렇게 라도 종료 메서드를 호출하는 게 더 낫기 때문입니다.
Timer 클래스를 보면 finalize() 메서드를 통해 안정망 역할을 구현했습니다.
Native란 자바 외의 C나 C++ 같은 프로그래밍 언어를 의미하며, Native API를 이용하여 이 코드와 연관된 자바 객체(Native peer 객체)를 만듭니다.
Native peer 객체는 일반 자바 객체가 아니므로, GC가 되지 않을 수 있습니다.
이런 경우엔 Finalizer를 이용할 수 있습니다.
마지막으로 이용 시 주의할 점에 대해 알아보겠습니다.
chaining이란 상속 관계에 있는 클래스에서 서브 클래스의 메서드 호출 시, 이와 연관된 슈퍼 클래스의 메서드가 자동으로 호출되는 것을 말합니다.
예를 들면 어떤 클래스가 Finalizer 메서드를 가지고 있고, 서브 클래스에서 그 메서드를 override 한다면, 서브 클래스의 Finalizer 메서드에서 슈퍼 클래스의 Finalizer를 반드시 호출해 줘야 합니다.
@Override
protected void finalize() throws Throwable {
try {
// 서브 클래스를 Finalize 하는 코드
} finally {
super.finalize();
}
}
만약 위에서 처럼 서브 클래스에서 슈퍼 클래스의 Finalizer를 override 하고 호출하지 않는 다면, 당연한 말이지만 슈퍼 클래스의 Finalizer는 절대 실행되지 않습니다.
이를 대비해 Finalizer guardian을 이용할 수 있습니다.
아래 코드를 보면 Foo 클래스는 finalize() 메서드를 가지고 있지 않습니다.
따라서 서브 클래스의 finalize 메서드에서 super.finalize를 호출하는지 신경 쓸 필요가 없습니다.
// Finalizer guardian
public class Foo {
// 이 객체의 목적은 외부 클래스(Foo) 객체의 Finalize를 수행하는 것입니다.
private final Object finalizerGuardian = new Object() {
@Override
protected void finalize() throws Throwable {
... // 외부 클래스(Foo) 객체의 Finalize를 수행하는 코드
}
}
}
정리하면 종료 메서드 호출을 빼먹는 경우를 대비한 안전망 역할이나, Native 자원을 종결하는 경우를 제외하면 Finalizer를 이용하지 맙시다.
그리고 만약 쓴다면 super.finalize 메서드를 호출하는 것 을 잊지 말고, 그도 안된다면 Finalizer guardian을 이용합시다.
Finalizer 그동안 생각 안 해봤는데, 이상한 개념? 들이 많네요.
쓰지 맙시다ㅠ