Item 8. finalizer와 cleaner 사용을 피하라

심규환·2022년 1월 15일
0

Effective Java

목록 보기
8/29
post-thumbnail

자바에는 두 가지의 객체 소멸자를 제공합니다. finalizercleaner인데. 이 두 가지의 사용은 피하는게 좋습니다.
finalizer는 심각한 위험성을 가지고 있는데. 성능 저하, 오동작, 이식성 문제등 쓰면 안되는 여러가지 이유가 있습니다.

finalizer와 cleaner는 제때 실행되어야 하는 작업에는 절대 할 수 없습니다. 바로 스레드의 우선 순위가 낮아서 언제 실행될 기회를 얻기 힘들기 때문에 바로 바로 진행해야 하는 작업에 사용한다면 계속 뒤로 밀려나 OutOfMemoryError의 주범이 됩니다.

또 자바 언어 명세에는 finalizer나 cleaner의 수행 시점뿐만 아니라 수행 여부도 보장하지 않습니다. 제대로 작업을 끝내지도 못하고 프로그램이 중단될 수 있습니다. 그렇기 때문에 상태를 영구적으로 수정하는 데이터베이스의 자원을 건드리는 작업에는 사용하지 않아야 합니다.
finalizer의 부작용은 또 있습니다. 바로 동작 중에 발생한 예외는 무시되며 처리할 작업이 남았더라도 종료됩니다. 그렇기 때문에 불완전한 객체가 만들어 질 수 있습니다. 이러한 불안전한 객체를 사용하여 공격에 노출될 수도 있습니다.
그리고 finalizer와 cleaner는 심각한 성능 문제도 동반합니다. 간단한 객체를 생성하고 가비지 컬렉터가 수거하는 시간과 이 두 소멸자를 사용하여 객체를 파괴하는 과정과는 거의 50배가 차이가 납니다.

대신해서 사용할 묘안은 무엇일까요? AutoCloseable을 구현해주고, 클라이언트에서 인스턴스를 다 쓰고 나면 close 메서드를 호출하면 됩니다. 동시에 예외가 발생하더라도 제대로 종료될 수 있도록 try-with-resources를 사용해야 합니다.

그렇다면 이 쓰지못할 두 소멸자를 어떤 곳에 써야 할까요? 두 가지 사용 예시가 있습니다. 하나는 클라이언트가 close를 호출하지 못할 경우의 안전망 역할을 합니다. 우선 순위가 낮아 언제 수행할지 모르지만 아예 안하는 것보단 나으지 최후의 보루로서 사용하는 것입니다.
두 번재 예는 네이티브 피어(native peer)와 연결된 객체에서 입니다. 네이티브 피어란 일반 자바 객체가 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말합니다.
네이티브 피어는 가비지 컬렉터의 범위에 벗어났기 때문에 cleaner나 finalizer로 처리하기에 적합한 작업입니다. 단, 성능 저하를 감당할 수 있고 네이티브 피어가 심각한 자원을 가지고 있지 않을 때에만 해당됩니다.

다음은 AutoClosable에서 cleaner를 안정망으로 사용한 예시입니다.

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() throws Exception {
        cleanable.clean();
    }
}

코드를 간단하게 설명하자면 State 안에 numJunkPiles는 남은 자원들이고 close()를 호출하면 cleaner를 통해 자원을 청소하게 됩니다.
만약 클라이언트가 close() 호출을 깜빡한다면 가비지 컬렉터가 Room을 회수할 때 State의 run()을 호출해줄 것입니다.
State 인스턴스는 '절대로' Room 인스턴스를 참조해서는 안 됩니다. 그 이유는 순환 참조가 발생해서 가비지 컬렉터가 Room 인스턴스를 회수할 기회가 오지 않기 때문입니다.

Room의 cleaner는 단지 안전망으로만 쓰인 것이고 클라이언트가 Room 생성을 try-with-resources 블록으로 감쌌다면 자동 청소는 전혀 필요하지 않다.

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

위의 코드는 "안녕"이 출력된 후, 이어서 "방 청소"를 출력한다.

profile
장생농씬가?

0개의 댓글