Java finalize() 은퇴식

allocProc
5 min readMar 12, 2023

--

Photo by mk. s on Unsplash

Java Object 클래스의 메소드 중에는 finalize() 라는 녀석이 있습니다. 아주 오래전 Java 태동기부터 존재했고 그 설계 의도도 명확했죠. 하지만 최근 몇년간 많은 문제점과 논란이 제기된 끝에 JDK 9 버전에서 Deprecated 되었고 결국 사라질 준비를 하고 있습니다. (JEP-421)

finalize()?

finalize() 메소드는 모든 자바 클래스의 부모인 Object 클래스에 선언된 메소드 중 하나입니다. 객체가 가비지 컬렉터에 의해 수집될 때 실행되도록 설계되었습니다. 가비지 수집 대상이 되었을 때 애플리케이션 개발자가 의도한 기능을 수행하여 별도의 리소스 정리 작업을 할 수 있도록 했습니다.

클래스의 생성(초기화) 시점에 리소스를 획득하고 종료 시점에 리소스를 다시 반납하는 패턴을 RAII(Resource Acqusition Is Initialization) 패턴 이라고 부릅니다. C++ 에서 주로 사용되는 패턴인데, GC 가 따로 없다보니 개발자가 직접 리소스를 할당하고 해제해주어야 했고 까딱 잘못하면 메모리 누수가 나곤했죠. 그래서 위 처럼 패턴을 만들어서 고착화 시킨겁니다. 클래스의 사용 종료는 동시에 리소스의 반납을 보장하는거죠. (RAII 패턴에 대한 인상 깊은 글 하나 소개합니다: https://occamsrazr.net/tt/297)

그리고 Oracle 의 문서는 다음과 같이 설명합니다.

  • 해당 객체를 가리키는 레퍼런스가 없을 때 가비지 컬렉터에 의해 호출된다.
  • 리소스 반납이나 별도의 정리 작업을 수행해야한다.
  • Object 클래스의 finalize() 는 그저 비어있는 메소드일 뿐(no-op)이라서 따로 오버라이드 하지 않을 경우 아무 동작도 수행하지 않는다.
  • 어떤 스레드가 finalize() 를 호출할지는 모른다.
  • finalize() 를 호출하는 스레드는 어떠한 user-visible synchronization lock 도 잡고있지 않는다.
  • finalize() 내부에서 발생한 exception 은 무시된다.
  • 한 객체에 대해 두 번 이상 호출되지 않는다.

GC 의 특별 대우

다만 finalize() 메소드를 오버라이드 하면 해당 객체는 가비지 컬렉터가 조금 특별하게 처리합니다. Soft/Weak 레퍼런스들이 다르게 처리되는 것 처럼요. 가바지 수집 즉시 회수되지 않고 종료화 대상으로 먼저 등록됩니다. 그래서 finalize() 를 오버라이드 하지 않는 객체보다 수명이 한 사이클 정도 더 길다고 볼 수 있죠. 좀 더 상세하게 단계별로 정리하면 다음과 같습니다.

  1. finalize() 를 오버라이드 한 객체는 큐로 이동
  2. 별도의 finalize 스레드가 해당 큐를 비우면서 각 객체마다 정의된 finalize() 메소드를 호출
  3. finalize() 가 종료되면 해당 객체는 다음 GC 사이클에 진짜 수집될 준비를 마침

가비지 컬렉터가 finalize() 메소드를 오버라이드 한 객체와 그렇지 않는 객체를 구별할 수 있는 이유는 객체가 finalize() 메소드를 오버라이드 하게되면 해당 객체의 생성자 바디가 성공적으로 반환되는 시점에 해당 객체를 종료화 가능한 객체 목록에 등록하는 식으로 JVM 에 구현되어 있기 때문입니다.

finalize() 의 문제점

  1. 객체의 수명 연장
    바로 위에서 설명한 것처럼 finalize() 메소드를 오버라이드한 객체들은 일반 객체들 보다 한 사이클정도 수명이 깁니다. 보통은 이 하나의 사이클이 별거 아닌 것 처럼 느껴지겠지만 tenured 세대에 있는 객체들은 이 사이클이 상당히 긴 시간이 될 수 있습니다. 이는 곧 리소스가 낭비되는 시간이 더 길어진다는 의미이구요.
  2. 예외 처리
    Oracle 문서에도 명시되어 있듯이 finalize() 메소드 실행 도중 발생한 예외는 아무도 처리할 수 없습니다. finalize() 메소드를 실행하는 스레드는 유저 애플리케이션 컨텍스트가 없기 때문이죠. 그리고 finalize() 를 실행하는 스레드는 또 별도로 생성되기 때문에 이 오버헤드 또한 감수해야합니다.
  3. 언제 실행될지 알 수 없다.
    개발자가 동적 메모리를 수동으로 처리하는 C++ 와 달리 Java 는 할당할 가용 메모리가 부족하면 가비지 콜렉터가 그때그때 반사적으로 가비지 수집을 수행합니다. 가비지 수집이 언제 일어날지 아무도 모르기 때문에 finalize() 메소드도 언제 실행될 지 알수가 없습니다. 가비지 수집 자체가 언제 실행될지 알 수 없는 상황에서 리소스 반납을 finalize() 에 맡긴다는 것 자체가 어불성설인거죠.

위 같은 문제점들 때문에 Oracle 은 예전부터 일반적인 애플리케이션 코드에 finalize() 를 사용하지 말라고 개발자들에게 권고해왔습니다. 그러다가 JDK 9 부터는 deprecate 되었구요.

finalize() 는 런타임 내부 깊은 곳에 있는 어셈블리 코드에 기반해 객체를 미리 등록하고 조금 특별한 GC 작업을 수행합니다. 그런 다음 별도의 finalize 스레드와 큐를 통해 정리 작업을 합니다.
finalize() 는 절대적으로 GC 에 의존하며 GC 는 그 자체로 불확정적이고 예측 불가능해서 리소스 관리를 비롯하여 대부분의 경우에 본래 의도와는 맞지 않을 가능성이 높습니다.

저 개인적으로도 finalize() 메소드를 직접 오버라이드 해본 적은 없고 Java 를 처음 배울 때 교재에서 개념정도만 짚고 넘어간 기억이 있는데요. 초기 Java 개발자들이 이 메소드를 개발한 의도는 명확하고 이해 가능하지만 GC 도 없던 태동기 Java 때 개발되었기 때문인지 GC 와 엮어서 생각해보면 영 궁합이 좋지 않아 보이네요. 최근 버전의 Oracle 문서에는 곧 삭제 될거라고 경고까지 되어있습니다.

--

--