[Why]
- finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있으니 기본적으로 '쓰지 말아야' 한다.
- cleaner 역시 예측할 수 없고, 느리고, 일반적으로 불필요하다.
- finalizer 동작 중 발생한 예외는 무시되며, 처리할 작업이 남았더라도 그 순간 종료된다. 보통의 경우에는 예외가 발생하여 스택 추적 내역을 출력하겠지만, finalizer에서는 경고조차 출력되지 않는다.
- finalizer와 cleaner는 성능도 심각하다. 가비지 컬렉터보다 50배 정도 느리다.
- finalizer를 사용한 클래스는 finalizer 공격에 노출되어 심각한 보안 문제를 일으킬 수도 있다. 생성자나 직렬화 과정에서 만들어지다 만 객체를 악용할 수 있다.
[When]
파일 닫기
- finalizer와 cleaner로는 제때 실행되어야 하는 작업은 절대 할 수 없다.
- 그래서 파일 닫기를 finalizer나 cleaner에 맡기면 중대한 오류를 일으킬 수 있다. 시스템이 동시에 열 수 있는 파일 개수에 한계가 있기 때문이다.
DB의 영구 lock 해제
- 상태를 영구적으로 수정하는 작업에서는 절대 finalizer나 cleaner에 의존해서는 안 된다.
- finalizer나 cleaner는 수행 시점뿐 아니라 수행 여부조차도 보장하지 않기 때문이다.
cleaner를 안정망으로 활용하는 AutoCloseable 클래스
※ 방(room) 자원을 수거하기 전에 반드시 청소(clean)해야 한다고 가정해보자.
public class Room implements AutoCloseable {
private static final Cleaner cleaner = Cleaner.create();
// 청소가 필요한 자원, 절대 Room을 참조해서는 안 된다!
private static class State implements Runnable {
int numJunkPiles; // 방(Room) 안의 쓰레기 수
State(int numJunkPiles) {
this.numJunkPiles = numJunkPiles;
}
// close 메서드나 cleaner가 호출된다.
@Override
public void run() {
System.out.println("방 청소");
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();
}
}
- State는 Runnable을 구현하고, 그 안의 run 메서드는 cleanable에 의해 딱 한 번만 호출될 것이다.
- cleanable 객체는 Room 생성자에서 cleaner에 Room과 State를 등록할 때 얻는다.
- State가 정적 중첩 클래스인 이유 : 정적이 아닌 중첩 클래스는 자동으로 바깥 객체의 참조를 갖는다아이템24. 그러면 순환참조가 생겨 가비지 컬렉터가 Room 인스턴스를 회수해갈 기회가 오지 않는다.
- run 메서드가 호출되는 상황은 둘 중 하나다.
- Room의 close 메서드를 호출할 때다. close 메서드에서 Cleanable의 clean을 호출하면 이 메서드 안에서 run을 호출한다.
- 가비지 컬렉터가 Room을 회수할 때까지 클라이언트가 close를 호출하지 않는다면, cleaner가 State의 run 메서드를 호출해줄 것이다.
// 잘 짜인 클라이언트 코드
public class Adult {
public static void main(String[] args) {
try (Room myRoom = new Room(7)) {
System.out.println("안녕~");
}
}
}
- 기대한대로 Adult 프로그램은 "안녕~"을 출력한 후, 이어서 "방 청소"를 출력한다.
// 잘 못 짠 클라이언트 코드
public class Teenager {
public static void main(String[] args) {
new Room(99);
System.out.println("아무렴");
}
}
- "아무렴"에 이어 "방 청소"가 출력되지 않는다. 앞서 '예측할 수 없다'고 한 상황이다.
[How]
- finalizer나 cleaner 대신, AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 된다. 일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resources를 사용해야 한다. 아이템9
- 자바에서의 비메모리 자원 회수는 try-with-resources와 try-finally를 사용해 해결한다아이템9.
- final이 아닌 클래스를 finalizer 공격으로부터 방어하려면 아무 일도 하지 않는 finalize 메서드를 만들고 final로 선언하자.
'독서찰기(讀書札記) > 이펙티브 자바' 카테고리의 다른 글
[아이템 10] equals는 일반 규약을 지켜 재정의하라 (0) | 2022.01.08 |
---|---|
[아이템 9] try-finally보다는 try-with-resources를 사용하라 (0) | 2022.01.08 |
[아이템 7] 다 쓴 객체 참조를 해제하라 (0) | 2022.01.08 |
[아이템 6] 불필요한 객체 생성을 피하라 (0) | 2022.01.07 |
[아이템 5] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2022.01.06 |