자바는 두가지 객체 소멸자를 제공한다. 그 중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있다. 아예 사용하지 않아야 한다. 그래서 자바9부터는 아예 Deprecated됐다. 또 다른 하나로는 cleaner가 있다. 이 cleaner는 finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고 역시나 일반적으로 불필요하다.
자바 언어 명세에서 finalizer, cleaner는 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다. 그러니까 제때 실행되어야 하는 작업은 저 두가지로 절대 할 수 없다.
그럼 자바에서 적절하게 객체를 소멸하는 방법은 뭐가 있을까?
→ AutoCloseable을 구현한 객체를 try-with-resource 구문으로 처리하는 것이다.
(클라이언트에서 인스턴스를 다 쓰고 나면 AutoCloseable이 구현해야 하는 메서드인 close 메서드를 호출해도 되지만, 일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resource 구문을 사용해야 한다)
AutoCloseable을 구현한 객체를 try-with-resource 구문으로 처리할 때, 구체적으로 알아두면 좋을 내용이 있다.
→ 각 인스턴스는 자신이 닫혔는지를 추적하는 것이 좋다. 다시 말해, close 메서드에서 이 객체는 더 이상 유효하지 않음을 필드에 기록하고, 다른 메서드는 이 필드를 검사해서 객체가 닫힌 후에 불렸다면 IllegalStateException을 던지는 것이다.
그럼 finalizer는 Deprecated 됐다고 치면, cleaner는 여전히 사용가능하게 되어 있는데 도대체 어디에 쓰는 물건인고? 싶을것이다.
적절한 쓰임새가 한가지 정도 있다고 보는데, 자원의 소유자가 close 메서드를 호출하지 않는 것에 대한 안전망 역할이다. 즉, 사용자가 다 쓰고 close 메서드를 호출하지 않거나 try-with-resource 구문도 사용하지 않는 그런 예. 그렇다고 cleaner가 즉시 호출되리라는 보장은 없지만 아예 자원 회수를 안하는 것 보다는 나으니 말이다.
자바 라이브러리도 이와 유사한 클래스가 있다. FileInputStream, FileOutputStream 등등이 말이다.
그래서 가장 좋은 방법은 cleaner를 사용한 클래스를 AutoCloseable로 구현하고 사용할 땐 try-with-resource 구문을 꼭 사용하는데 까먹고 사용하지 않은 경우 cleaner에게 제발 실행만이라도 되도록 기도하는 것이다.
다음은 cleaner만 사용한 예시이다. 어떻게 사용하는지는 알아야 하니까.
import java.lang.ref.Cleaner;
class Resource {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public Resource() {
// 리소스 초기화
System.out.println("리소스가 할당되었습니다.");
// 리소스가 수거될 때 수행할 작업 정의
this.cleanable = cleaner.register(this, () -> {
System.out.println("리소스가 해제되었습니다.");
});
}
public void close() {
cleanable.clean(); // 명시적으로 클리닝 작업 실행 가능
}
}
public class CleanerExample {
public static void main(String[] args) {
Resource resource = new Resource();
resource.close(); // 명시적으로 리소스를 해제
}
}
근데 완전하지 않으니 AutoCloseable까지 구현해보자. try-with-resource 구문을 사용해서 반드시 객체를 소멸시키게 만들 수 있도록 말이다.
Resource (AutoCloseable + Cleaner)
package items.item8;
import java.lang.ref.Cleaner;
public class Resource implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
private final Cleaner.Cleanable cleanable;
public Resource() {
// 리소스 초기화
System.out.println("리소스가 할당되었습니다.");
// 리소스가 수거될 때 수행할 작업 정의
this.cleanable = cleaner.register(this, () -> {
System.out.println("리소스가 해제되었습니다.");
});
}
public void close() {
cleanable.clean(); // 명시적으로 클리닝 작업 실행 가능
}
}
- AutoCloseable과 Cleaner를 같이 사용한다. Cleaner만으로는 확신을 가질 수 없다. 그래서 AutoCloseable의 close 메서드안에 Cleaner를 사용해 자원을 정리하게 한다.
package items.item8;
public class Main {
public static void main(String[] args) {
try (Resource r = new Resource()) {
System.out.println("안녕?");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
실행결과
리소스가 할당되었습니다.
안녕?
리소스가 해제되었습니다.
Process finished with exit code 0
- 완벽하게 close 메서드가 실행됐고 원하는대로 자원을 소멸시킨다.
그런데 만약, 이렇게 호출하면 어떻게 될까?
package items.item8;
public class Main {
public static void main(String[] args) {
Resource r = new Resource();
System.out.println("안녕?");
}
}
실행결과
리소스가 할당되었습니다.
안녕?
Process finished with exit code 0
- Cleaner가 실행되지 않았다. 이렇다. cleaner는 믿을게 못된다. 그래서 안전망 역할 정도만을 기대하는 게 가장 좋다. 절대 AutoCloseable을 두고 Cleaner를 사용하려고 하지마라.
참고로, 다음과 같이 System.gc()를 호출해서는 정상적으로 리소스가 종료됐지만 이걸 따라하고 있는 당신도 그러리란 보장이 없다.
package items.item8;
public class Main {
public static void main(String[] args) {
Resource r = new Resource();
System.out.println("안녕?");
System.gc();
}
}
'Effective Java' 카테고리의 다른 글
아이템 7 - 다 쓴 객체 참조를 해제하라 (3) | 2024.09.16 |
---|---|
아이템 6 - 불필요한 객체 생성을 피하라 (2) | 2024.09.16 |
아이템 4 - 인스턴스화를 막으려거든 private 생성자를 사용하라. (0) | 2024.09.16 |