finalizer와 cleaner는 C++의 파괴자와는 다른 개념이다.
C++에서의 파괴자는 생성자의 꼭 필요한 대척점으로 특정 객체와 관련된 자원을 회수하는 보편적인 방법이다.
하지만 자바에서는 접근할 수 없게 된 객체를 회수하는 역할을 가비지 컬렉터가 담당하고, 프로그래머에게는 아무런 작업도 요구하지 않는다.
C++의 파괴자는 비메모리 자원을 회수하는 용도로 쓰인다. 하지만 자바에서는 try-with-resources와 try-finally를 사용해 해결한다.
자바 9 이전에는 finalizer를, 이후에는 그 대안으로 cleaner를 제공하지만 둘 다 위험하고, 예측할 수 없으며 느리고 일반적으로 불필요하다.
단점을 살펴보자.
1. finalizer와 cleaner는 즉시 수행된다는 보장이 없다.
객체에 접근할 수 없게 된 후 finalizer와 cleaner가 실행되기까지 얼마나 걸릴지 알 수 없다. 즉 제때 실행되어야 하는 작업은 절대 할 수 없다. 두 함수가 얼마나 신속히 수행할지는 전적으로 가비지 컬렉터 알고리즘에 달렸으며, 이는 가비지 컬렉터 구현마다 천차만별이다.
2. 수행 시점뿐 아니라 수행 여부조차 보장하지 않는다.
접근할 수 없는 일부 객체에 딸린 종료 작업을 전혀 수행하지 못한 채 프로그램이 중단될 수도 있다. 따라서 생애 주기와 상관없는, 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존하지 말자.
예를 들어 DB 같은 공유 자원의 영구 락 해제를 맡겨 놓으면 분산 시스템 전체가 서서히 멈출 것이다.
System.gc나 System.runFinalization 메서드는 finalizer와 cleaner가 실행될 가능성을 높여줄 수는 있으나, 보장해주지는 않는다.
3. finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다.
예외를 처리하지 못해 해당 객체는 마무리가 덜 된 상태도 남을 수 있고, 다른 스레드가 이처럼 훼손된 객체를 사용하려 한다면 어떻게 동작할지 예측할 수 없다. 보통의 경우에는 잡지 못한 예외가 스레드를 중단시키고 스택 추적 내역을 출력하지만, finalizer에서는 경고조차 출력하지 않는다.
cleaner를 사용하는 라이브러리는 자신의 스레드를 통제하기 때문에 이러한 문제는 발생하지 않는다.
4. 심각한 성능 문제를 동반한다.
AutoCloseable 객체를 생성하고 가비지 컬렉터가 수거하기까지 12ns가 걸리는 반면, finalizer를 사용하면 550ns나 걸린다. 무려 50배나 차이 난다.
5. finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 가진다.
생성자나 직렬화 과정에서 예외가 발생하면, 이 생성되다 만 객체에서의 악의적인 하위 클래스의 finalizer가 수행될 수 있게 된다. 이 finalizer는 정적 필드에서 자신의 참조를 할당하여 가비지 컬렉터가 수집하지 못하게 막을 수 있고, 이렇게 일그러진 객체가 만들어지고 나면 객체의 메서드를 호출해 허용되지 않은 작업을 수행할 수 있다.
오버라이딩한 finalizer 메서드를 final로 만들면 상속받지 못하여 문제를 해결할 수 있다.
finalizer 예시
public class SampleRunner {
public static void main(String[] args) throws InterruptedException{
SampleRunner runner = new SampleRunner();
runner.run();
Thread.sleep(1000l);
}
private void run(){
FinalizerExample finalizerExample = new FinalizerExample();
finalizerExample.hello();
}
}
public class FinalizerExample {
//이 메서드는 가비지 컬렉션 될 때 호출됨
@Override
protected void finalize() throws Throwable{
System.out.println("Clean up");
}
public void hello(){
System.out.println("hello");
}
}
SampleRunner 클래스에서 run 메서드가 호출되고 난 후에 finalizerExample 객체는 유효하지 않다.
finalizerExample은 gc의 대상이 되지만 gc가 언제 될지는 모른다. 따라서 gc가 될 때 호출되는 finalize 메서드가 1초 뒤에도 호출되지 않고 프로그램이 종료된다.
파일이나 스레드 등 종료해야 할 자원을 담고 있는 객체의 클래스에서 finalizer와 cleaner를 대신하는 방법은 무엇일까?
AutoCloseable을 구현해 주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출해 주자.
public class SampleResource implements AutoCloseable{
@Override
public void close() throws RuntimeException{
System.out.println("close");
}
public void hello(){
System.out.println("hello");
}
}
1. try-finally
public class SampleRunner {
public static void main(String[] args) throws InterruptedException{
SampleResource sampleResource = null;
try{
sampleResource = new SampleResource();
sampleResource.hello();
} finally { //예외가 있든 없든 무조건 실행됨
if(sampleResource !=null){
sampleResource.close(); //close를 호출
}
}
}
}
2. try-with-resources
public class SampleRunner {
public static void main(String[] args) throws InterruptedException{
//명시적으로 close를 호출하지 않아도 try가 끝날때
// AutoCloseable의 close를 호출해준다.
try(SampleResource sampleResource = new SampleResource()){
sampleResource.hello();
}
}
}
그렇다면 이렇게 단점 밖에 없는 finalizer와 cleaner는 왜 존재할까. 한번 살펴보자.
1. 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할
finalizer 또는 cleaner가 즉시 호출되리라는 보장은 없지만, 클라이언트가 하지 않은 자원 회수를 늦게라도 해주는 것이 아예 안 하는 것보다 낫다. 자바 라이브러리의 일부 클래스는 안전망 역할의 finalizer를 제공하는데, FileInputStream, FileOutputStream, ThreadPoolExecutor가 대표적이다.
1. finalizer로 안전망 사용
public class SampleResource implements AutoCloseable{
private boolean closed;
@Override
public void close() throws RuntimeException{
if(this.closed){
throw new IllegalStateException();
}
closed = true;
System.out.println("close");
}
public void hello(){
System.out.println("hello");
}
@Override
protected void finalize() throws Throwable{
if(!this.closed) close();
}
}
2. cleaner로 안전망 사용
import java.lang.ref.Cleaner;
public class SampleResource implements AutoCloseable{
private boolean closed;
private static final Cleaner CLEANER = Cleaner.create();
private final Cleaner.Cleanable cleanable;
private final ResourceCleaner resourceCleaner;
public SampleResource(){
this.resourceCleaner = new ResourceCleaner();
this.cleanable = CLEANER.register(this, resourceCleaner);
}
private static class ResourceCleaner implements Runnable{
@Override
public void run(){
System.out.println("Clean");
}
}
@Override
public void close() throws RuntimeException{
if(this.closed){
throw new IllegalStateException();
}
closed = true;
cleanable.clean();
}
public void hello(){
System.out.println("hello");
}
}
2. 네이티브 피어와 연결된 객체에서의 사용
네이티브 피어 : 일반 자바 객체가 자바 이외의 다른 언어를 통해 작성된 네이티브 메서드를 통해 기능을 위임한 네이티브 객체
네이티브 피어는 자바 객체가 아니니 가비지 컬렉터는 그 존재를 알 수 없다. 따라서 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못한다.
이러한 경우에 finalizer와 cleaner를 사용할 수 있는데, 이 또한 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당한다. 그렇지 않다면 close 메서드를 사용하자.
https://velog.io/@sa1341/AutoCloseable-%ED%81%B4%EB%9E%98%EC%8A%A4
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 11. equals를 재정의 하려거든 hashCode도 재정의 하라 (2) | 2023.03.08 |
---|---|
[Effective Java] Item 10. equals는 일반 규약을 지켜 재정의 하라 (0) | 2023.03.07 |
[Effective Java] Item 9. try-finally 보다는 try-with-resources 를 사용하라 (0) | 2023.03.07 |
[Effective Java] Item 7. 다 쓴 객체 참조를 해제하라 (0) | 2023.03.04 |
[Effective Java] Item 1~6. 객체 생성과 파괴 (2) | 2023.03.01 |