Item8 - finalizer와 cleaner 사용을 피하라

2023. 11. 19. 19:14Book/이펙티브 자바

 

들어가기 전 요약

 

 - 반납할 자원이 있는 클래스는 AutoCloseable을 구현하고 클라이언트에서 close()를 호출하거나 try-with-resource를 사용해야 한다.

 

 

 

자바는 두가지 객체 소멸자를 제공한다.

 1. finalizer : 예측할 수 업고, 상황에 따라 위험할 수 있어 일반적으로 불필요하다

 2. cleaner : finalizer보다는 덜 위험하지만, 여전히 예측할 수 없고, 느리고, 일반적으로 불필요하다.

 

단점

 - finalizer와 cleaner는 즉시 수행된다는 보장이 없다.

 - finalizer와 cleaner는 실행되지 않을 수도 있다.

 - finalizer 동작 중에 예외가 발생하면 정리 작업이 처리되지 않을 수도 있다.

 - finalizer와 cleaner는 심각한 성능 문제가 있다.

 - finalizer는 보안 문제가 있다.

public class Account {

    private String accountId;

    public Account(String accountId) {
        this.accountId = accountId;

        if (accountId.equals("푸티")) {
            throw new IllegalArgumentException("푸티는 계정을 막습니다");
        }
    }

    public void transfer(BigDecimal amount, String to) {
        System.out.printf("transfer %f from %s to %s\n", amount, accountId, to);
    }
}


=====================================================================================


public class BrokenAccount extends Account{

    public BrokenAccount(String accountId) {
        super(accountId);
    }

    @Override
    protected void finalize() throws Throwable {
        this.transfer(BigDecimal.valueOf(100000), "son");
    }
}

Account 클래스가 있고 특정 계좌 주인의 이름에는 에러를 던지는 조건이 있다고 했을 때 해당 클래스를 상속해서 finalize를 오버라이딩한 클래스를 실행시면 어떻게 되는지 보자

 

    @Test
    void 푸티_공격_계정() throws InterruptedException {
        Account account = null;
        try {
            account = new BrokenAccount("푸티");
        } catch (Exception e) {
            System.out.println("이러면???");
        }
        System.gc();

        Thread.sleep(3000L);
        //account.transfer(BigDecimal.valueOf(10.4), "hello");
    }
    
     ==>
    이러면???
	transfer 100000.000000 from 푸티 to son

부모 클래스의 생성자 메서드를 호출해 에러가 발생해도 상속받은 클래스의 finalize 메서드가 호출되어 심각한 공격을 받을 수 있다.

 이러한 공격을 막으려면 finalize 클래스를 부모 클래스에서 final로 오버라이딩해 자식 클래스가 더이상 오버라이딩 하지 못하게 해야한다.

 

 

쓰임새

 - 자원의 소유자가 close 메서드를 호출하지 않는 것에 대비한 안전망 역할 : 즉시 회수된다는 보장은 없지만 지연 회수를 하는 것이 안하는 것만 낫다

// 코드 8-1 cleaner를 안전망으로 활용하는 AutoCloseable 클래스 (44쪽)
public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    // 청소가 필요한 자원. 절대 Room을 참조해서는 안 된다! => 순환 참조 일어날 수 있음
    private static class State implements Runnable {
        int numJunkPiles; // Number of junk piles in this room

        State(int numJunkPiles) {
            this.numJunkPiles = numJunkPiles;
        }

        // close 메서드나 cleaner가 호출한다.
        @Override public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }

    // 방의 상태. cleanable과 공유한다.
    private final State state;

    // cleanable 객체. 수거 대상이 되면 방을 청소한다.
    private final Cleaner.Cleanable cleanable;

    public Room(int numJunkPiles) {
        state = new State(numJunkPiles);
        cleanable = cleaner.register(this, state);
    }

    @Override public void close() {
        cleanable.clean();
    }
}

=======================================================================================

close() 메서드 호출

public class Adult {
    public static void main(String[] args) {
        try (Room myRoom = new Room(7)) {
            System.out.println("안녕~");
        }
    }
}

=======================================================================================

안전망
public class Teenager {
    public static void main(String[] args) {
        new Room(99);
        System.out.println("Peace Out");

        //이런식으로 가비지 컬렉터를 강제로 호출하면 안된다
        //System.gc();
    }
}

State 메서드는 중첩클래스이기 때문에 static으로 선언했다. 그렇지 않다면 바깥 클래스인 Room의 레퍼런스르 가지고 있기 때문에 순환참조가 일어날 수 있다.

 

 

 - 네이티브 피어(자바 객체가 아님)와 연결된 객체에서 가비지 컬렉터가 그 존재를 알지 못하니 객체 소멸자를 이용해 회수한다.(단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당된다.) 웬만하면 close() 메서드를 사용하자