[이펙티브 자바] 객체의 생성과 파괴 Item8 - finalizer와 cleaner 사용을 피하라

이성훈·2022년 3월 21일
0

이펙티브 자바

목록 보기
9/17
post-thumbnail

이펙티브 자바의 첫 시작은 객체를 생성하고 파괴하는 것에 대한 고찰이다.

"2장 - 객체의 생성과 파괴" 는 다음과 같은 기준으로 맥락을 잡고 있다.

  • 객체를 만들어야 할 때는 언제인가
  • 객체를 만들지 말아야 할 때는 언제인가
  • 올바른 객체 생성 방법은 무엇인가
  • 객체의 불필요한 생성을 피하는 방법은 무엇인가
  • 객체를 제 때에 파괴시키는 방법은 무엇인가
  • 파괴 전에 수행해야 할 정리 작업을 관리하는 요령이 있는가

위와 같은 맥락을 계속 기억하며 공부하자.


  • Item1. 생성자 대신 정적 팩터리 메서드를 고려하라.
  • Item2. 생성자에 매개변수가 많다면 빌더를 고려하라.
  • Item3. private 생성자나 열거 타입으로 싱글턴임을 보증하라.
  • Item4. 인스턴스화를 막으려거든 private 생성자를 사용하라.
  • Item5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라.
  • Item6. 불필요한 객체 생성을 피하라.
  • Item7. 다 쓴 객체 참조를 해체하라.
  • Item8. finalizer와 cleaner 사용을 피하라.
  • Item9. try-finally 보다는 try-with-resources를 사용하라.




<"finalizer와 cleaner 사용을 피하라">



#   finalizer와 cleaner



자바에서는 두 가지의 객체 소멸자를 제공하고 있다.

낮은 버전에서 finalizer라는 소멸자를 제공하고 있었는데,
예측할 수 없고, 상황에 따라 위험할 수 있다는 단점이 발견되었다.

이는 오동과 낮은 성능, 이식성 등의 문제 원인이 될 수 있어 사용이 지양되었다.


그래서 자바9 에서부터는 cleaner라는 새로운 객체 소멸자를 제공하기 시작한다.

finalizer보다는 덜 위험하지만,
여전히 예측할 수 없고, 느리고, 굳이 필요하지가 않다.


여기서 finalizer와 cleaner는 자원을 회수하는 가비지컬렉터와는 다른 개념이다.




  • "즉시 수행된다는 보장이 없다"

finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
다시 말해, 그 둘이 실행되기까지 얼마나의 시간이 걸릴지 알 수가 없다는 말이다.
(자칫하면 영영 실행 안될수도 있다.)

그렇기에 꼭 제때에 실행되어야 하는 로직에 대해서는 finalizer와 cleaner는 매우 취약하다.

Comment.

예를 들어서, 파일 닫기라는 기능을 finalizer나 cleaner에게 맡긴다고 가정해보자.

언제 실행될 지 알수가 없어 파일을 닫히지 않고 있는데, 새로운 파일이 계속 열리게 되면 어떨까?

시스템이 동시에 열 수 있는 파일의 갯수는 한정되어 있기 때문에 그 임계점을 넘어가게 되면 결국 프로그램 자체가 실패하게 된다.


여기서 의문을 품는 사람들이 있을 것이다.

그럼 finalizer와 cleaner를 빨리 수행될 수 있게 하면 되는거 아닌가?

이는 전적으로 가비지컬렉터 알고리즘에 달려있다.
즉, JVM 환경마다 다르게 돌아간다는 말이다.

테스트 할 때는 괜찮았는데 실제 환경에서 갑자기 안될 수 있다.
그건 매우 리스크가 크다.


실제로 finalizer는 현업에서도 많은 문제를 일으킨다.

finalizer 스레드 자체가 다른 어플리케이션 스레드보다 우선순위가 낮기 때문에,
실행 대기열에서 계속 순서가 밀리면서 자원 회수가 지연될 수 있다.

cleaner의 경우 자기 자신을 수행할 스레드를 제어할 수 있지만,
여전히 가비지컬렉터 통제하에 있기 대문에 즉각 수행된다는 보장이 없다.



  • "수행 시점뿐 아니라 수행 여부조차 보장하지 않는다."

쉽게 말해, 프로그램이 끝났을때 과연 원하고자 했던 자원 회수가 됬는지 안됬는지조차 알 수가 없다는 말이다.

그렇기 때문에,
상태를 영구적으로 수정하는 작업에서는 (프로그램이 죽건 말건 상관이 없는),
절대 finalizer와 cleaner에 의존해서는 안된다.



  • "finalizer에서 발생한 예외는 무시되며, 처리할 작업이 남아있어도 종료된다."

결국 예외 하나 때문에 객체 자체가 마무리가 덜 된 상태로 프로그램이 끝날 수도 있다는 말이다.

그렇게 훼손된 상태로 끝난 객체를 또 다른 자원에서 사용하려고 한다면,
더 큰 문제를 야기할수도 있을것이다.


또 원래는 예외가 발생하면 스레드가 중단되고 스택 추적 내용이 나오지만,
finalizer는 경고조차 나오지 않는다.

그나마 cleaner에서는 이러한 문제가 없다.



  • "finalizer와 cleaner는 심각한 성능 문제도 동반한다."

finalizer는 사용 그 자체로 가비지컬렉터의 효율을 떨어뜨린다.
cleaner도 클래스의 모든 인스턴스를 수거하는 형태로 사용할 경우 finalizer와 비슷하다.

자원 회수에 finalizer를 사용할 경우, try-with-resources를 사용했을 때보다 50배가 느려지가 된다.



  • "finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다."

finalizer 공격이란, 객체가 생성되는 과정에서 예외가 발생했을 때,

이 생성되다 만 객체의 하위 클래스에서 악의적으로 finalizer가 수행될 수 있게 하는 것이다.

<finalizer 공격>

  • 객체가 생성되는 과정에서 예외가 발생한다.
  • 생성되다 만 객체의 하위 클래스에서 finalizer가 수행되도록 한다.
  • finalizer는 정적 필드에 자신의 참조를 할당하여 가비지컬렉터가 스스로를 수집할 수 없게 한다.
  • 의도치 않은 메서드 수행 등으로 악의적인 공격이 가능하다.

finalizer 공격은 충분히 끔찍한 결과를 초래할 수 있다.

이러한 공격을 방지하기 위한 방법은,
아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하면 된다.




#   해결 방법


결국 finalizer와 cleaner 모두 사용하지 않는게 좋다는 것을 알게 되었다.




  • AutoCloseable 구현하기

그렇지만 자원 관리를 위해 객체 소멸의 기능을 하는 무언가는 분명 필요하긴 하다.


이럴때는 AutoCloseable을 구현해주면 된다.

즉, 클라이언트는 인스턴스를 다 쓰면 close 메서드를 호출하여 자원을 반납한다.
( 일반적으로는 try-with-resources를 사용한다. )

위와 같이 구현할 경우,
각 인스턴스는 자신이 닫혔는가에 대해 추적하는 것이 좋다.

이를 위해서, close 메서드에서 이 객체가 더 이상 유효하지 않다는걸 기록한다.
그런 후에 이 객체를 어디선가 부르려고 할 때 예외를 던지도록 하면 된다.



  • finalizer와 cleaner의 적절한 사용?

어쨋든 우리는 위와 같은 맥락에서,
finalizer와 cleaner를 객체 소멸자 용도로 쓰는 것은 좋지 않다는 것을 배웠다.

그러면 대체 어디서 쓰는 것이 적절할까?



  • 안전망으로써의 finalizer와 cleaner

쉽게 말하자면 보험용으로 사용하자는 것이다.

클라이언트가 close 메서드를 호출하지 않으면 자원 회수가 되지 않을것이므로,
그럴 경우에 대비한 안정망 역할로 사용하는 것이다.


자원 회수를 언제 할지 모른다는거야 같지만,
클라이언트가 close 메서드를 깜빡하고 호출 안해서 영영 자원 회수가 안되는 것보다는 언젠가라도 되는 것이 좋지 않겠는가?



  • 네이티브 피어와 연결된 객체일때

<네이티브 피어 (Native Peer)>

  • 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 객체
  • 자바 객체가 아니므로 가비지 컬렉터가 회수하지 못함.

딱 위에 설명만 읽어봐도 finalizer와 cleaner가 나서기 좋은 포인트이다.

가비지컬렉터가 나서서 회수할 수 없으니, 대체품으로 끼워넣기 딱 좋지 않은가.

다만, 만약 그러한 네이티브 피어가 가지고 있는 자원이 매우 크다면 그냥 close를 쓰는 것이 좋다.

언제 회수 될지도 모르는데 자원이 큰 객체가 계속 살아있으면 성능저하를 일으킨다.




지금까지 cleaner와 finalizer 사용을 하면 안되는 이유에 대해서 알아보았다.

필자의 코멘트를 마지막으로 글을 마무리한다.


Item8 정리

  • cleaner와 finalizer는 안정망 역할로 사용하자.
  • 또는 중요하지 않은 네이티브 자원 회수 용으로 사용하자.
  • 그럼에도 불구하고 불확실성과 성능 저하에는 주의해야 한다.

profile
IT 지식 공간

0개의 댓글