Study/이펙티브 자바 / / 2023. 8. 25. 18:42

[Effective Java 3E] finalizer와 cleaner 사용을 피하라

💥 개요

자바에서는 2가지 객체 소멸자를 제공합니다. 그중 finalizer는 예측할 수 없고, 상황에 따라 위험할 수 있어 일반적으로 불필요합니다.(앞으로 개발하면서 쓸 일이 없을지도...) 심지어 자바9에서 deprecated API 로 지정되었고, 그 대안으로 cleaner를 제시했지만, cleaner 역시 위험하고, 예측할 수 없고, 느리고, 불필요합니다.

 

🩻 문제

언제 수행될지 모른다

finalizer와 cleaner는 즉시 수행된다는 보장이 없습니다.

객체에 접근할 수 없게 된 후 finalizer와 cleaner로는 제때 실행되야 하는 작업은 절대 할 수없습니다.(파일, db처럼 사용 후 즉시 반납되어야 하는 자원에 절대 사용X)

finalizer와 cleaner가 언제 수행될지는 전적으로 GC 알고리즘에 달렸으며, 천차만별입니다. finalizer나 cleaner에 의존하는 프로그램의 동작 또한 마찬가지입니다. 

로컬 환경의 JVM에서는 완벽하게 동작하던 프로그램이 실제 서버에서는 엄청난 문제를 일으킬 수 있습니다.

어플리케이션에서 객체 수천개가 finalizer 대기열에서 회수되기만을 기다리고, finalizer 스레드는 우선 순위가 낮아서 제대로 실행될 기회도 얻지 못한채로 OOM이 발생할 수 있습니다. 이를 해결할 방법은 단 하나, finalizer를 사용하지 않는 방법 뿐 입니다.

cleaner는 조금 낫지만, 즉각 수행되리라는 보장이 없습니다. DB에서 lock 해제와 같은 작업을 finalizer나 cleaner를 통해서 사용시 분산시스템 전체가 멈출 가능성이 높습니다. 이를 보장해주는 메서드가 있지만 Thread가 멈추는 치명적인 문제가 있습니다.

 

심각한 성능 문제

간단한 AutoCloseable 객체를 생성하고 가비지 컬렉터가 수거하기까지 12ns가 걸리지만, finalizer는 550ns가 걸리며 이는 약 50배나 느린 문제가 있습니다. finalizer가 가비지 컬렉터의 효율을 떨어뜨리기 떄문입니다. cleaner도 모든 인스턴스를 수거하는 형태로 사용하면 비슷합니다. 하지만 안정망 형태로 사용하면 66ns로 약 5배정도 느려집니다.

 

심각한 보안 문제

finalizer를 사용한 클래스는 finalizer 공격에 노출되어 아래처럼 심각한 보안 문제를 일으킬 수 있습니다..

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");
	}

}

잔액을 관리하는 클래스입니다.

이 객체에서는 isAuthorized는 항상 false를 return하기 때문에 instance를 만들어 transferMoney를 보낼 수 없습니다.

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 attack을 하는 객체입니다. 잔액을 관리하는 AccountOperations 객체를 상속하여 finalize 메소드를 override 했습니다.

import java.util.concurrent.TimeUnit;

public class App {

	public static void main(String args[]) throws InterruptedException {
		AccountOperations accOperations = null;

		try {
			accOperations = new FakeAccountOperations();
		} catch (Exception e) {
			System.out.println(e.getMessage());
		}

		System.gc();
		TimeUnit.MINUTES.sleep(10);
	}

}

이 코드의 Output은 어떻게 될까요?

new FakeAccountOpertaions()를 실행하면 exception이 일어나게 될텐데 transfer 메서드가 실행될까요?

Output
You can't access the account
Still I can transfer money
Transferring 100.0 to beneficiary
 
FakeAccountOperations 클래스를 상속하고 finalize 메서드를 override하였습니다. 'finalize' 메소드는 객체가 가비지 컬렉터의 대상이 되기전에 Java 런타임에 의해 호출되고, 치명적인 메서드가 실행됐습니다.

 

👍 해결방법

1.final 클래스로 선언

public final 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");
	}

}

 

2. finalize 메서드를 final로 정의

final protected void finalize() {
		
	}
 

3. 메서드 내부에서 검사

public class AccountOperations {
	private boolean isUserAuthorized = false;

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

	public boolean isAuthorized() {
		return false;
	}

	public void transferMoney(double amount) {
		if (!isUserAuthorized) {
			System.out.println("You are not authorized");
			return;
		}
		System.out.println("Transferring " + amount + " to beneficiary");
	}

}
 {
	private boolean isUserAuthorized = false;

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

	public boolean isAuthorized() {
		return false;
	}

	public void transferMoney(double amount) {
		if (!isUserAuthorized) {
			System.out.println("You are not authorized");
			return;
		}
		System.out.println("Transferring " + amount + " to beneficiary");
	}

}
 

🧲 finalizer와 cleaner의 대안

finalizer와 cleaner의 대안은 그저 AutoCloseable을 구현하고, 인스턴스를 다 사용한 다음 close 메서드를 호출하면 됩니다.
일반적으로 예외가 발생해도 제대로 종료되도록 try-with-resources를 사용 해야 합니다.
 

cleaner와 finalizer의 적절한 용도

close 호출을 안하는 것에 대한 안전망 역할

cleaner나 finalizer가 즉시 호출되리라는 보장은 없지만(실행 안될수도 있음), 클라이언트가 자원을 회수하지 않는다면 늦게라도 해주는게 나으니 안전망 역할의 finalizer를 작성할 수 있습니다.(진짜 그럴 가치가 있는지 심사수고 해야함)

 

네이티브 피어와 연결된 객체

네이티브 피어란 네이티브 메서드를 통해 기능을 위임한 네이티브 객체를 말합니다.

네이티브 피어는 자바 객체가 아니니 가비지 컬렉터는 그 존재를 알지 못하고, 자바 피어를 회수할 때 네이티브 객체까지 회수하지 못합니다. 이때 finalizer나 cleaner가 사용될 수 있지만 성능 저하와 심각한 자원을 가지고 있지 않을 때 해당됩니다. 아니라면 close 메서드를 사용해야 합니다.(어지간하면 사용하지 말라는 얘기)

책에서는 이 뒤에 cleaner에 관하여 더 예제를 통해 얘기를 하고 있습니다. 하지만 위에서 계속 언급했던, GC의 우선적인 대상이 되지 못하기 때문에 언제 수행될지 모르는 문제(심지어 System.gc()를 명시해도 즉시 수행되지 않음)를 말하고 있어 설명하지 않겠습니다.

 

핵심 정리

cleaner(Java 8까지는 finalizer)는 안전망 역할이나 중요하지 않은 네이티브 자원 회수용으로만 사용하자.
물론 이런 경우라도 불확실성과 성능 저하에 주의해야 한다.

cleaner나 finalizer 대신
autocloseable을 구현하여 close()를 사용하고 예외인 경우에도 close 될 수 있도록 
try-with-resources를 사용하면 될 것 같습니다.

 

 

❤️ 스터디 질답 정리

Q. cleaner의 수행여부를 보장하지 않는데 cleaner가 스레드를 제어하는게 어떤 의미인지?
A. 자기 자신만의 스레드를 갖고 있고, 순서 제어는 가능하지만 언제 실행되는지는 알 수 없다는 의미입니다.

Q. finalize attack의 해결방안에 다른건 어떤게 있을까요?
A. finalize 메서드에 final 키워드 붙이기, final 클래스로 만들기, 중요 로직을 한번 더 검사하기가 있습니다.

Q.네이티브 피어가 이해가 안가는게 조금 더 설명해주세요
A.다른 언어(C, C++)로 구현된 메서드를 export하여 java에서 사용하는 것을 네이티브 피어라고 니다.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유