본문 바로가기
Language/Java

이펙티브 자바 - 종료자 사용을 피하라

by ocwokocw 2021. 9. 22.

- 이 글은 Effective Java 를 기반으로 작성되었습니다.

- 종료자

Java 의 종료자(finalize)는 예측 불가능하며 대체로 위험하고 일반적인 상황에서는 불필요 하다. C++ 소멸자의 경우 메모리 반환과 자원반환의 일반적인 수단이지만, Java 의 경우 GC가 메모리를 정리해주고 자원반환과 같은 경우에도 try-finally 구문을 사용하기 때문에 정말 특수한 상황이 아니면 종료자를 사용할 필요가 없다.


- 종료자의 단점

종료자의 첫번째 단점은 예측이 되지 않는다는것이다. 즉시 실행이 보장되지 않는데 모든 참조가 사라지고 난 후 종료자가 실행되기까지에는 어느정도 간격이 존재한다. 그래서 특히 time-critical 한 작업에는 절대로 종료자는 사용하면 안된다. 만약 file close 를 try-finally 가 아니라 종료자에서 수행하면 파일 기술자(File description)라는 자원은 유한한데 JVM 은 종료자를 수행하기까지 어느정도 간격이 존재하므로 열린 상태의 파일이 많이 남아 있게 된다. 이때 파일을 새로 열려고 하면 개수에 제한이 있어 오류가 날 수 있다.

 

즉시실행이 되지 않는것뿐 아니라 Java 에는 반드시 종료자를 실행해야 한다는 명세도 없다. 심지어 System.gc 로 수동으로 GC 를 수행해도 보증해주지 않는다. (실전에서는 System.gc 메소드는 STW를 일으키기 때문에 절대로 사용하면 안된다.)

 

두번째 단점은 종료자 처리도중 예측 불가능한 예외가 발생해도 무시하며 종료과정이 중단된다. 또한 종료자 사용을 성능을 심각하게 떨어뜨릴수도 있다.


- 자원의 반환

종료자를 사용하면 안된다는것은 알겠는데 그래서 종료자를 사용하지 않고 자원을 어떻게 반환할것인가?

 

명시적인 종료 메서드를 정의하고 호출하는것이 좋다. 이미 Java 에서 파일처리나 JDBC 기초 프로그래밍을 해보았다면 이런 처리를 명시적으로 해본 경험이 있을것이다.

 

public class TryCatch {

	public static void main(String[] args) throws IOException {
	
		InputStreamReader isr = null;
		Scanner scan = null;
		try {
		
			isr = new InputStreamReader(System.in);
			scan = new Scanner(isr);
			
			String test = scan.next();
			System.out.println("test: " + test);
			
		} catch (Exception e) {
			
		} finally {
			isr.close();
			scan.close();
		}
	}
	
}

 

위와 같이 finally 안에서 자원들을 종료해준다. 만약 JDk 1.7 이상이라면 try-with-resources 구문을 사용하면 finally 에서 close() 를 명시적으로 호출하지 않아도 편리하게 자원을 반환할 수 있다.

 

public class TryCatchWithResources {

	public static void main(String[] args) throws IOException {
	
		try (InputStreamReader isr = new InputStreamReader(System.in);
			Scanner scan = new Scanner(isr);){
		
			String test = scan.next();
			System.out.println("test: " + test);
			
			
		} catch (Exception e) {
			
		}
	}
}

 

훨씬 편리하게 자원을 사용하고 코드도 깔끔해졌다. 그러나 아무 자원이나 try-with-resources 구문을 사용할 수 있는것은 아니다. AutoCloseable 인터페이스를 구현한 자원만 사용할 수 있다.

 

*
 * @author Josh Bloch
 * @since 1.7
 */
public interface AutoCloseable {

- 종료자를 사용해야 하는 경우

자원반환시에는 명시적인 종료 메소드를 호출하는게 좋다. 때문에 자원 반환을 하는데 종료자를 사용하는것은 좋지 않지만 방어적인 개념으로는 사용할 수 있다.

 

첫째로 명시적인 종료호출을 잊었을때를 대비해 종료자에서 자원반환을 하면 속도가 늦긴 하겠지만 반환이 될 것이다. 다만 이때 종료자가 호출되는것은 명시적인 종료 메소드가 호출되지 않는다는 뜻이므로 log 를 남기는것이 좋다.

 

둘째로 네이티브 피어(네이티브 메소드를 통해 기능 수행을 위임하는 네이티브 객체)를 사용할 때 이다. 네이티브 영역이기 때문에 GC 가 알 수 없어 종료자 사용을 하기에 알맞다. 하지만 만약 중요한 자원을 점유하고 있다면 사용 후 명시적인 메소드를 호출해주는 것이 좋기 때문에 네이티브 피어에서의 종료자 사용도 중요하지 않은 자원일 경우에만 해당하는 얘기가 된다.

 

셋째로 상속을 받는 경우 이다. 어떤 클래스 종료자에서 자원의 반환을 하였고, 이 클래스를 상속 받는 경우 자식에서 종료자 호출시에 자동으로 부모의 종료자를 호출하지 않는다. 따라서 종료자에서 부모의 종료자를 명시적으로 호출해주어야 한다.

 

public class Parent {

	@Override
	protected void finalize() throws Throwable {
		
		System.out.println("finalize Parent");
		super.finalize();
	}
}

..........

public class Child extends Parent{

	@Override
	protected void finalize() throws Throwable {
		
		try {
			System.out.println("finalize Child");
		} finally {
			super.finalize();
		}
	}
}

- 종료 보호자

상속을 받아 자식 클래스에서 종료자를 재정의하는 경우 자식에서 부모의 종료자를 호출하지 않는 문제가 생길 수 있다. 이때 종료 보호자를 이용하면 이 문제를 방지할 수가 있다. 익명 클래스를 만들어서 finalize 를 재정의한 종료 보호자를 private 필드로 참조하는것인데 코드를 한번 살펴보자.

 

public class Child extends Parent{

	private final Object finalizerGuardian = new Object() {

		@Override
		protected void finalize() throws Throwable {
			System.out.println("finalize Child");
			super.finalize();
		}
	};
}

 

위의 코드에서 Child(바깥 객체에 대한 참조) 에 대한 참조가 모두 사라지는 순간 Object() 익명 클래스의 종료자도 수행이 가능한 상태가 된다. 여기에서 반환작업을 수행하면 된다.

댓글