이펙티브 자바 3판 - 아이템 8. finalizer와 cleaner 사용을 피하라.

김대협·2023년 2월 24일
0

Effective Java 3rd

목록 보기
8/9

아이템 8. finalizer와 cleaner 사용을 피하라.


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

JDK 9부터 finalizer는 deprecated로 지정되었고 cleaner가 대안으로 등장했다.

finalizer는 예측할 수 없고, 상황에 따라 위험하고, 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.
cleaner는 위보단 낫지만, 여전히 예측할 수 없고, 느리고, 일반적인 상황에 불필요하다.

본래의 사용 목적은 반납할 자원이 있거나, 리소스를 제거할 때 사용하라고 구현된 부분이지만 사실상 사용하지 않음이 좋다.

finalizer, cleaner를 사용하지 말아야 하는 이유

  • finalizer, cleaner는 원하는 순간에 수행된다는 보장이 없다.
  • 실행 자체가 진행되지 않을 수 있다.
  • 예외가 발생하면 무시되고, 누수가 발생할 수 있다.
  • finalizer는 심각한 성능과 보안 문제가 있다.

Finalizer Attack 예시

상속을 통한 finalize 메서드를 재정의하여 인위적인 GC를 발동시켜, 차단된 유저가 Account를 이용할 수 있는
Finalizer Attack과 테스트의 예시 코드이다.

public class Account {
    private final String accountId;

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

        if ( "김무스".equals( accountId ) ) {
            throw new IllegalArgumentException( "is blacklist user" );
        }
    }

    public void transferMoney( String receiverId, long transferAmount ) {
        System.out.println( "transfer " + transferAmount + " to " + receiverId );
    }
}

public class BrokenAccount extends Account {
    public BrokenAccount( String accountId ) {
        super( accountId );
    }

    @Override
    protected void finalize() throws Throwable {
        this.transferMoney( "K", 1_000_000 );
    }
}

class AccountTest {
    @Test
    @DisplayName( "정상 유저의 송금 테스트" )
    void transferNormalUser() {
        Account account = new Account( "김대협" );
        account.transferMoney( "K", 1_000_000 );
    }

    @Test
    @DisplayName( "블랙 유저의 송금 테스트" )
    void transferBlackUser() {
        Account account = new Account( "김무스" );
        account.transferMoney( "K", 1_000_000 );
    }

    @Test
    @DisplayName( "블랙 유저의 Finalizer Attack" )
    void transferBlackUserForFinalizerAttack() throws InterruptedException {
        Account account = null;
        try {
            account = new BrokenAccount( "김무스" );
        } catch ( Exception e ) {
        }

        System.gc();
        TimeUnit.SECONDS.sleep( 3 );
    }
}

finalizer attack을 방어하기 위해서는 final class를 정의하거나 정의할 수 없다면 finalize를 재정의 할 수 없도록 구현해야 한다.

public class Account {
    private final String accountId;

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

        if ( "김무스".equals( accountId ) ) {
            throw new IllegalArgumentException( "is blacklist user" );
        }
    }

    public void transferMoney( String receiverId, long transferAmount ) {
        System.out.println( "transfer " + transferAmount + " to " + receiverId );
    }

    // finalize를 재정의할 수 없도록 final로 재정의를 막는다.
    @Override
    protected final void finalize() throws Throwable {
        super.finalize();
    }
}

자원 회수의 대안

Finalizer를 대신하여 사용할 수 있는 가장 적절한 대안이 AutoCloseable을 구현한 try-with-resources 사용이다.
이를 이용하면 예외가 발생하더라도 누수없이 종료할 수 있다.

public class AutoCloseableImpl implements AutoCloseable {
    private BufferedReader reader;

    public AutoCloseableImpl( String path ) {
        try {
            this.reader = new BufferedReader( new FileReader( path ) );
        } catch ( FileNotFoundException e ) {
            throw new RuntimeException( e );
        }
    }

    @Override
    public void close() throws Exception {
        try {
            reader.close();
        } catch ( IOException e ) {
            throw new IOException( e );
        }
    }
}

class AutoCloseableImplTest {
    @Test
    void closeableTest() {
        try( AutoCloseableImpl autoCloseable = new AutoCloseableImpl( "file_path" ) ) {
        } catch ( Exception e ) {
            throw new RuntimeException( e );
        }
    }
}

가급적이면 close 방식은 idempotent(멱등성)을 보장해야 한다.

idempotent는 연산을 여러 차례 적용하더라도 결과가 달라지지 않는 성질을 의미한다.

finalizer와 cleaner는 언제 활용할 수 있는가?

일반적으로 finalizer, cleaner의 사용은 안전망 용도로의 사용만 할 수 있도록 최소화 하자.

1) 자원의 소유자가 회수를 안할 경우, 대비하는 안전망 용도

  • 호출의 보장은 할 수 없으나, 자원 누수보다 회수 시도라도 하는 것이 안전망의 역할
  • 안전망을 구성할 떄는 성능상의 이슈를 감당할 가치가 있는지 우선 판단하여야 한다.
  • 자바의 일부 라이브러리의 클래스는 안전망 역할로 finalizer를 제공하고 있다.

finalize를 안전망 용도로 사용하는 라이브러리의 종류
FileInputStream, FileOutputStream, ThreadPoolExecutor

2) native peer와 연결된 객체에 사용하자

native peer는 일반 객체가 native 메서드를 통해 기능을 위임한 native 객체를 뜻한다.
native peer는 GC가 그 존재 유무를 알 수 없다.
성능 저하를 감당할 수 있고 중대한 자원은 갖고 있지 않을때 활용할 수 있다.


© 2023.1 Written by Boseong Kim.
profile
기록하는 개발자

0개의 댓글