본문 바로가기

STUDY/이펙티브자바

2-8) finalizer와 cleaner 사용을 피하라

객체 소멸자

 

class MyObject {
    // 생성자
    MyObject(){
    }
    // 소멸자
    ~MyObject(){
    }
}
// 메모리 할당
MyObject my_obj = new MyObject();
...
// 메모리 해제 > 소멸자 호출됨
delete my_obj;

 

C++은 객체 생성시에 실행되는 생성자와  객체 소멸시에 실행되는 소멸자가 있다. delete 키워드로 객체를 소멸시키면 소멸자가 호출된다. 소멸자는 객체가 사용하던 자원을 정리하는데 쓰인다. 생성자에서 "temp.txt" 파일의 읽기/쓰기 스트림을 열었다면, 소멸자에서 스트림을 닫는 동작을 수행한다. Java에도 이런 역할을 하는 finalizer와  cleaner가 있다.

 

 


finalizer와 cleaner

 

C++에서 소멸자는 좋은 코드를 만드는 키워드이다. 하지만 Java에서 finalizer와 cleaner는 사용이 권장되지 않는다. 이 두 키워드는 아래와 같은 문제점이 있다.

  1. 실행 시점에 대한 보장이 없다.

  2. 실행 여부에 대한 보장이 없다.

  3. 성능 저하를 유발한다.

  4. 보안 문제를 유발한다.

 

public class MyObject {
    private FileOutputStream os;
	
    public MyObject() throws IOException{
    	os = new FileOutputStream("test.txt");
    	os.getChannel().lock();
    }
    
    protected void finalize() throws IOException{
    	os.close();
    }
}

 

MyObject myObj1 = new MyObject();
myObj1 = null;
System.gc();
MyObject myObj2 = new MyObject();

위와 같은 클래스와 코드가 있다고 했을 때, 개발자는 myObj1을 null로 만들고 gc를 실행함으로써 myObj1.finalize()가 실행되길 기대하였다. 그리고 실제로 필자의 환경에서 코드를 실행해보면 기대한대로 동작한다. 하지만 이는 매우 잘못된 코드이다. JVM마다 GC가 동작하는 방식이 다르다. 따라서 myObj1을 null로 만들었다고 해서 실행환경의 JVM이 myObj1에 들어있던 데이터를 GC의 대상으로 인식했을지는 알 수 없다. GC가 동작하는 시점또한 알 수 없다. System.gc()를 호출하여도 실제로 GC동작을 수행할지 여부는 JVM이 판단한다. 따라서 finalizer, cleaner를 사용한 코드는 실행 시점과 여부가 불확실하다. 결론적으로 위의 코드는 실행환경에따 오류가 발생하는 위험한 코드이다.

 

// AutoCloseable과 try-with-resources를 사용한 경우
// 약 60ms
for(int i = 0; i < 100; i++) {			
    try(MyObject myObj1 = new MyObject()) {
    } catch (Exception e) {
    }
}

// finalizer와 gc를 사용한 경우
// 약 200ms
for(int i = 0; i < 100; i++) {			
    MyObject myObj1 = new MyObject();
    myObj1 = null;
    System.gc();
}

finalizer와 cleaner를 사용은 성능 저하를 유발할 수 있다. MyObject의 인스턴스를 생성하고 소멸시키는 동일한 의도의 코드가 AutoCloseable을 사용할 경우 60ms, finalizer를 사용할 경우 200ms가 소요된다.

 

public class AccountOperations {
    public AccountOperations() {
        if (!isAuthorized()) {
            throw new SecurityException("You can't access the account");
        }
    }

    public boolean isAuthorized() {
        return false;
    }

    public void transferMoney(double amount) {
        System.out.println("Transferring " + amount + " to beneficiary");
    }
}

 

public class FakeAccountOperations extends AccountOperations {
    public FakeAccountOperations() {
    }

    @Override
    protected void finalize() {
        System.out.println("Still I can transfer money");
        this.transferMoney(100);
        System.exit(0);
    }
}

마지막으로 finalizer를 사용한 글래스는 보안상의 위협을 유발할 수 있다. 위의 예제에서 AccountOperations 클래스 객체를 생성할 수 없고 따라서 transferMoney() 메소드도 호출할 수 없다. 하지만 이를 상속한 FakeAccountOperations 클래스의 finalize() 메소드에서 transferMoney() 메소드를 호출하고 있고, 실제로 실행이 된다. 이처럼 finalizer는 개발자가 의도하지 않은 동작을 유도하여 보안상의 위협이 된다.

 

 


AutoCloseable

 

public class Room implements AutoCloseable {
    private static final Cleaner cleaner = Cleaner.create();

    private static class State implements Runnable {
        int numJunkPiles; // Number of junk piles in this room

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

        @Override public void run() {
            System.out.println("Cleaning room");
            numJunkPiles = 0;
        }
    }

    private final State state;

    private final Cleaner.Cleanable cleanable;

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

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

결론적으로 finaliser와 cleaner는 사용이 권장되지 않는 키워드이다. 그렇다면 객체애서 사용하는 리소스를 해제하기 위해서는 어떻게 해야할까? 바로 AutoCloeaable을 상속하고, try-with-resources 문을 사용하는 것이다. 그리고 try-with-resources를 사용하지 않을 경우를 대비한 안전망으로 cleaner를 사용한다. 그렇게 하면 일반적인 경우 AutoCloseable의 close()가 호출되어 효율적으로 자원을 반환할 수 있고, 개발자가 try-with-resources를 사용하지 않은 상황에서도 cleaner 덕분에 언젠가는 자원이 반납되리라고 기대할 수 있다.

728x90