Java에는 소멸자가 없나요?

급식·2025년 1월 10일
0

Java

목록 보기
3/3
post-thumbnail

어라라?

가만히 생각해보면 c++에서는 GC가 아닌 사람이 맨파워로 메모리를 할당해서 쓰기 때문에, 반대로 해제 작업이 필요하다. 자원을 쓴 다음 닫아 놓는 것은 기본 중의 기본이지만 메모리 관리의 책임까지 개발자에게 있는 unmanaged language에서는 맘 편하게 객체 정리될 때 알아서 실행되는 메서드가 있으면 좋을 것 같다. 그래서 있는게 객체의 소멸자 메서드이다.

근데 보통 Java 같은 managed language에서는 GC나 그에 준하는 도구 덕분에 메모리 수준에서는 이러한 걱정을 잘 안 한다. 그래서 소멸자에 대해 생각을 딱히 안 하고 있었는데, Spring bean 생명 주기에서 Spring container 내부에서의 관리 종료 단계를 듣고 Java엔 명시적인 destructor가 없는지 좀 궁금해졌다.

이대로 쫑,,! 하긴 좀 아쉬워서 왜 deprecated 된 건지 먼저 알아봤습니다.

finilize?

작동 순서

  1. 객체가 더 이상 필요하지 않을 때, 레퍼런스 카운트가 0이 되었을 때 GC의 대상이 된다.
  2. GC가 객체의 공간을 회수하기 전에, JVM이 finilize 메서드를 호출한다.
  3. finalize 메서드에는 보통 객체가 사용하던 자원을 해제하거나, 정리하는 등의 작업을 수행하게 된다.
  4. 이 메서드가 실행이 완료되면, 객체는 GC에 의해 실제로 메모리에서 제거된다.

왜 쓰면 안되는가

GC가 유독 많이 나오는데, 명시적인 C++의 destructor 트리거와 달리 finilize 메서드는 GC에 의존한다. 그리고 GC는 언제 작동할지 모른다. 설사 finalize를 override 하여 하위 클래스 내부에서 사용한다고 하더라도, JVM이 무조건 한 번 실행하도록 되어 있으므로 JVM과 싸워서 이길 수 있는거 아니면 직접 쓸 생각은 말자.

또한 finilizing을 실행하는 JVM 내부의 스레드는 다른 일반적인 스레드들보다 우선 순위가 낮다고 한다. 즉, 시스템이 엄청 바쁠 때에는 일종의 기아 상태에 빠질 수 있어 의도한대로 객체의 소멸과 거의 동시에 자원이 해제됨을 보장할 수 없다는 말이다.

애초에 GC의 역할은 메모리의 관리이지, 여타 다른 자원를 고려하지 않고 설계되었다. 그렇기에 finilizing 스레드를 걱정하기 이전에, 시스템 차원의 메모리 누수로 인한 성능 저하부터 고민해야 할 것이다.

게다가 finilizer는 무섭게도 비동기적으로 실행된다고 한다. 아직 남아 있는 다른 객체와 해제되는 자원이 연관되어 있는 경우엔 당연히 문제가 발생할 것이고, 예외도 그냥 무시되기 때문에 GC의 본 목적인 GC의 메모리 정리를 방해할 것이다.

마지막으로,, 이게 제일 흥미로운데 소스 코드를 읽어 finilize를 재정의한 클래스가 객체의 소멸을 방해(구체적으로는 레퍼런스 카운트를 올려서 구제)함으로써 GC에 의해 제거되지 않도록 만들 수 있다. 또한 애플리케이션의 정상적인 흐름이 아닌 GC 레이어에서 일어나는 일이기에, 일반적으로는 불가능한 보안 관련 행위를 할 수 있게 된다. 사용이 완료된 데이터의 값을 읽는다던가,, 약간 쓰레기 고쳐다가 재활용하는 느낌이다.

Cleaner

그렇게 총알 구멍이 나버린 finalize 메서드를 대체하여, Cleaner라는 녀석이 대체제로 소개되었었다.
상속도 못 하도록 finialize 된 녀석인데, 일단은 finilize 메서드와 동일한 목적을 위해 설계되었다.

finalize보다 Cleaner가 나은 점

우선 GC 단에서 뭔가가 벌어지는게 아니라, 상대적으로 애플리케이션 레이어에 가깝게, 자체적으로 관리하는 스레드를 사용하여 정리 작업을 수행하기 때문에 러시안 룰렛 같은 finalize 보다야 훨씬 안정적이다. 게다가 그냥 스레드도 아니고 정리 작업에 최적화된 나이스한 스레드라고 한다,,!

근데 쓰면 안됩니다.

여전히 GC에 의해 자원이 회수된 이후에 비동기적으로 자원을 정리하기 때문에 정리 작업의 시점을 명확히 알 수 없기 때문이다. 🥲

내가 가겠다.

AutoCloseable

아마 많이 들어보셨을 인터페이스인데, close() 메서드를 구현하여 그 속에서 자원을 정리하면 된다.

이건 객체가 임종 직전이거나 돌아가셨을 때 실행되는게 아니라, 무덤으로 들어가기 전 애플리케이션 실행 컨텍스트 내에서 정리한 다음 레퍼런스 카운트가 0으로 떨어져 GC의 대상이 되는, 약속된 프로토콜이기에 훨씬 안전하다.

try-with-resources

이건 안지 얼마 안 된 건데, try문 안에서 아래와 같이 선언해서 AutoCloseable을 구현하는 클래스를 인스턴스화 하면 try 블럭을 벗어날 때 close 메서드를 호출해주는 깜찍한 녀석이다. (출처)

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    e.printStackTrace();
}

요 애물단지 녀석!

finalize야 단칼에 deprecated 되었다지만, Cleaner는 왜 아직 날아가지 않았을까? 크게 두 가지 이유가 있다.

첫째로, try-with-resources를 통해 무슨 일이 있어서 정말 정말 중요한 자원이 처리되지 않는게 걱정된다면 정말 최종 방어선의 느낌으로 사용될 수 있다. 아예 해제가 안 되는 것보다는 언제 될지는 몰라도 일단은 기다려보는 편이 낫잖은가,,! 하지만 앞서 소개한 단점이 정말 어마어마하기에,, 쓰기 전에 두 번 세 번 백 번 생각해야 한다.

둘째로, 네이티브 피어와 연결된 객체의 회수에 필요하다.
나도 몰라서 찾아보니까 특히 안드로이드 같은 환경에서 C나 C++ 같은 저수준 언어로 작성된 코드에 의해 생성되고 관리되는 객체랜다.

여하튼 이런 것들은 GC의 대상이 되지 않기 때문에, 직접 Cleaner를 사용해서 객체를 회수해줘야 한다고 한다.

이마저도 기존 레거시 시스템과의 호환성을 위해 남아 있기는 하지만, 언제 날아갈지 모르는 처지이므로 가장 먼저 명시적 리소스 해제로 비벼보되 진짜 진짜 정 안되면 Cleaner에 손을 대자.

profile
그래 다 먹고 살자고 하는 건데,, 🥹

0개의 댓글