객체 소멸자 finalizer와 cleaner를 쓰지 말아야 하는 이유
- finalizer - 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요 + 오동작, 낮은 성능, 이식성 문제의 원인
- cleaner - finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고 일반적으로 불필요
C++의 파괴자(destructor)와는 다른 개념이다.
자바에서 접근할 수 없게된 객체를 회수하는 역할(객체 소멸)은 GC(가비지 콜렉터)가 담당하고,
비메모리의 자원을 회수하는 역할은 try-with-resources와 try-finally를 사용
1. 즉시 수행된다는 보장이 없다.
객체에 접근 할 수 없게 된후 실행되기까지 얼마나 걸릴지 알 수 없다.
즉, 제때 실행되어야 하는 작업은 절대 할 수 없다.
finalizer와 cleaner를 얼마나 신속히 수행할지는 전적으로 GC알고리즘에 달려 있다.
(이는 GC구현마다 천차만별)
2. 자원회수가 제멋대로 지연된다.(수행시점 보장x)
finalizer 스레드는 다른 애플리케이션 스레드보다 우선 순위가 낮아서 실행될 기회를 제대로 얻지 못한다.
cleaner는 자신을 수행할 스레드를 제어할 수 있긴 하나, 여전히 백그라운드에서 수행되며 GC의 통제하에 있어서 즉각 수행된다는 보장x
3. 수행 여부도 보장되지 않는다.
접근할 수 없는 일부 객체에 딸린 종료 작업을 수행하지 못한 채 프로그램이 중단될 수도 있다.
따라서, 상태를 영구적으로 수정하는 작업에서는 finalizer나 cleaner에 의존해서는 안된다.
(ex. 공유 자원의 영구 lock해제를 finalizer나 cleaner에 맡겨놓으면 분산 시스템 전체가 서서히 멈추게 된다)
System.gc나 System.runFinalization 메서드는 실행될 가능성을 높여줄 수는 있으나, 보장하진 않는다.
System.runFinalozersOnExit와 Runtime.runFinalizersOnExit는 실행을 보장해 주긴 하지만, 다른 스레드가 소멸대상의 객체에 접근하고 있어도 실행해 버린다.
4. 동작 중 발생한 예외가 무시된다.
finalizer는 동작중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.
훼손된 객체(마무리가 덜된 객체)를 다른 스레드가 사용하려 한다면 어떻게 동작할 지 예측할 수 없다.
cleaner는 자신의 스레드를 통제하기 때문에 이러한 문제가 발생하지 않는다.
5. 심각한 성능 문제를 동반한다.
finalizer와 cleaner는 GC의 효율을 떨어뜨리기 때문에 심각한 성능 문제를 야기한다.
6. 보안 문제를 일으킬 수 있다.
생성자나 직렬화 과정에서 예외가 발생하면, 이 생성되다만 객체에서 하위 클래스의 finalizer가 수행될 수 있게 한다.
이 finalizer는 정적 필드에 자신의 참조를 할당해 GC가 수집하지 못하게 만든다.
-> 해법: final 클래스를 만들어 하위 클래스를 만들 수 없도록 하거나, final이 아닌 클래스인 경우 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언
finalizer나 cleaner를 대신할 해결책: AutoCloseable을 구현
파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰면 close 메서드를 호출하면 된다.
일반적으로 예외가 발생해도 제대로 종료가 되도록 하기 위해 try-with_resources를 사용한다.
이때 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다.
즉, close메서드에서 해당 객체가 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 그 필드를 검사해 객체가 닫힌 후에 불렀다면 IllegalStateException 을 던진다.
try-finally : 명시적으로 자원 반납
무조건 close 메서드가 호출되도록 하기 위해서는 try-finally block을 사용한다.
public class TryFinally implements AutoCloseable {
@Override
public void close() throws RuntimeException {
System.out.println("close");
}
public void hello() {
System.out.println("hello");
}
}
public class Runner {
public static void main(String[] args) {
try {
TryFinally tryFinally = new TryFinally();
tryFinally.hello(); // 리소스 사용
} finally {
tryFinally.close(); // 리소스를 사용하는 쪽에서 쓴 다음 반드시 close() 호출
}
}
}
try-with-resource : 암묵적으로 자원 반납(가장 이상적인 자원 반납 방법)
AutoCloseable을 구현한다면, 명시적으로 close 메서드를 호출하지 않아도 try블록이 끝날 때 자동을 close 메서드를 호출
public class TryWithResource implements AutoCloseable {
@Override
public void close() throws RuntimeException {
System.out.println("close");
}
public void hello() {
System.out.println("hello");
}
}
public class Runner {
public static void main(String[] args) {
try (TryWithResource resource = new TryWithResource()) {
resource.hello(); // 리소스 사용
}
}
}
finalizer와 cleaner가 사용되는 경우
1. 해당 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할
- finalizer나 cleaner가 호출된다는 보장은 없지만, 클라이언트가 하지 않은 자원회수를 늦게라도 해줄 수 있기 때문
(ex. FileInputStream, FileOutputStream, ThreadPoolExecutor, ..)
2. 네이티브 피어(native peer)와 연결된 객체
(네이티브 피어: 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체)
네이티브 피어는 자바 객체가 아니기 때문에 GC의 대상이 되지 않는다. 이때 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 갖고 있지 않다면, finalizer나 cleaner를 통해 늦게라도 자원회수 가능
하지만 네이티브 피어가 사용하는 자원을 즉시 회수해야 된다면 close 메서드 사용
cleaner는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용해야 한다.
이때 불확실성과 성능 저하에 주의해야 한다.
'Effective Java' 카테고리의 다른 글
[Effective Java] 아이템 10 - equals는 일반 규약을 지켜 재정의하라 (0) | 2022.01.19 |
---|---|
[Effective Java] 아이템 9 - try-finally 보다는 try-with-resources를 사용하라 (0) | 2022.01.11 |
[Effective Java] 아이템 7 - 다 쓴 객체 참조를 해제하라 (0) | 2022.01.11 |
[Effective Java] 아이템 6 - 불필요한 객체 생성을 피하라 (0) | 2022.01.10 |
[Effective Java] 아이템 4, 5 - 다중 instance 방지 & 의존성 주입(Dependency Injection) (0) | 2022.01.06 |