자바는 두 가지 객체 소멸자(finalizer
와 cleaner
)를 제공한다.
JDK 9부터 finalizer는 deprecated로 지정되었고 cleaner가 대안으로 등장했다.
finalizer는 예측할 수 없고, 상황에 따라 위험하고, 오동작, 낮은 성능, 이식성 문제의 원인이 되기도 한다.
cleaner는 위보단 낫지만, 여전히 예측할 수 없고, 느리고, 일반적인 상황에 불필요하다.
본래의 사용 목적은 반납할 자원이 있거나, 리소스를 제거할 때 사용하라고 구현된 부분이지만 사실상 사용하지 않음이 좋다.
상속을 통한 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의 사용은 안전망
용도로의 사용만 할 수 있도록 최소화 하자.
1) 자원의 소유자가 회수를 안할 경우, 대비하는 안전망 용도
finalize를 안전망 용도로 사용하는 라이브러리의 종류
FileInputStream, FileOutputStream, ThreadPoolExecutor
2) native peer와 연결된 객체에 사용하자
native peer는 일반 객체가 native 메서드를 통해 기능을 위임한 native 객체를 뜻한다.
native peer는 GC가 그 존재 유무를 알 수 없다.
성능 저하를 감당할 수 있고 중대한 자원은 갖고 있지 않을때 활용할 수 있다.