[Java] 메서드를 종료하는 방법 : finalizer

2022. 6. 3. 12:00JAVA/Overview

1. Finalizer의 사용

  • finalize() 메서드는 finalizer에 의해 호출됩니다.
  • Finalizer는 JVM이 특정 인스턴스를 회수해야 한다고 판달될 때 호출합니다. 이러한 finalizer는 인스턴스를 되살리는 것을 포함하여 임의의 동작을 수행할 수 있습니다.
  • finalizer의 주목적은 인스턴스가 사용하는 자원을 메모리에서 제거하기 전에 해제하는 것입니다.
  • finalizer는 자원 해제 작업을 위한 주요한 메커니즘을 작동하거나 다른 메서드들이 실패할 경우 안전망으로써 작동할 수 있습니다.

 

finalizer 작업이 어떻게 수행되는지 이해하기 위해서 다음 클래스 정의를 볼 수 있습니다.

public class Finalizable {
    private BufferedReader reader;

    // 생성자
    public Finalizable() {
    	
        InputStream input = Finalizable.class.getResourceAsStream("file.txt");
        this.reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }	
    // other class members
}

Finalizable 클래스는 자원을 회수할 수 있는 참조변수인 reader 필드멤버를 가지고 있습니다. Finalizable 타입의 인스턴스가 생성될 때 생성자에서 BufferedReader 인스턴스가 생성되고 reader 필드 멤버에 저장됩니다.

reader 참조변수는 readLine() 메서드를 호출하여 지정된 파일의 첫번째 줄을 추출하는데 사용됩니다. 여기서 주목할 점은 위 코드에서 reader는 close되지 않는다는 점입니다. 따라서 우리는 다음과 같이 finalizer를 사용할 수 있습니다.

	@Override
	protected void finalize(){
		try {
			reader.close();
			System.out.println("Closed BufferedReader in the finalizer");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

실제로 가비지 컬렉터가 finalizer를 호출하는 시기는 JVM의 구현과 우리가 제어할 수 없는 시스템의 조건에 따라 달라집니다. 즉석에서 자원을 회수할수 있도록 System.gc 메서드를 활용할 수도 있습니다. 하지만 실제 시스템에서는 다음과 같은 이유로 System.gc 메서드를 명시적으로 호출하는 것을 권하지 않습니다.

  1. System.gc 메서드를 호출하는 비용이 많음
  2. 가비지 컬렉션을 즉시 수행하지 않음
    • System.gc 메서드의 호출은 JVM이 가비지 컬렉션을 시작하기 위한 힌트일 뿐임
  3. JVM은 가비지 컬렉션을 호출해야 하는 시기를 더 잘 알고 있음

만약 우리가 가비지 컬렉션 수행이 필요하다면 다음과 같이 호출할 수 있습니다.

@Test
public void whenGC_thenFinalizerExecuted() throws IOException {
    String firstLine = new Finalizable().readFirstLine();
    assertEquals("baeldung.com", firstLine);
    System.gc();
}
  • new Finalizable() : Finalizable 익명 인스턴스를 생성함
  • new Finalizable().readFirstLine() : 익명 인스턴스가 readFirstLine() 메서드를 호출하여 "file.txt"의 한줄을 읽어와 반환함
  • System.gc() : 가비지 컬렉션을 명식적으로 호출함
실행결과
Closed BufferedReader in the finalizer

위 코드에서 System.gc 메서드를 명시적으로 호출하였을때 new Finalizable() 익명 인스턴스는 어떤 참조변수도 가리키지 않으므로 가비지 컬렉션의 대상에 알맞다는 것을 알 수 있습니다. 

위 테스트를 실행하면 콘솔에서 buffered reader 인스턴스가 close되었다는 메시지가 출력됩니다. 이는 finalize 메서드가 호출되었고 자원이 회수되었음을 추론할 수 있습니다.

 

여기까지는 finalizer가 사전 제거 작업을 위한 좋은 방법으로 보입니다. 그러나 이것은 완전히 사실이 아닙니다. 2장에서는 왜 finalizer를 피해야 하는지 볼것입니다.

 

2. Finalizer를 피해야 하는 이유

2.1 Finalizer의 단점

Finalizer는 즉시 수행되지 않는다

가비지 컬렉션이 언제든지 발생할 수 있기 때문에 finalizer가 수행되는 시기를 알 수 없습니다. finalizer가 조만간 실행되기 때문에 바로 문제가 되지는 않습니다. 그러나 시스템 자원이 무제한인 것은 아니므로 자원을 해제하기 전에 자원이 부족하여 시스템 충돌이 발생할 수 있습니다.

 

Finalizer는 프로그램의 이식성에 영향을 준다

Finalizer는 또한 프로그램의 이식성에도 영향을 끼칩니다. 가비지 컬렉션 알고리즘은 JVM 구현에 의존하기 때문에 프로그램은 다른 시스템에서 다르게 동작하는 동안 한 시스템에서 매우 잘 실행될 수 있습니다.

성능 비용은 Finalizer와 함께 발생하는 또 다른 중요한 문제입니다. 구체적으로 비어있지 않은 finalizer를 포함하는 인스턴스를 생성하고 파괴할때 JVM은 매우 많은 연산을 수행하기 때문입니다.

 

Finalizer는 예외가 발생시 인스턴스를 손상된 상태로 방치한다

마지막 문제점은 자원 해제 동안에 예외를 다루는 것의 부재입니다. 만약 finalizer가 예외가 발생하면 자원 해제 수행은 중단되고 인스턴스가 아무런 알림없이 손상된 상태로 유지됩니다.

 

2.2 Finalizer의 영향

다음 예제는 finalizer의 영향을 확인하는 예제입니다. 다음 클래스는 finalizer가 재정의된 클래스입니다.

public class CrashedFinalizable {
    public static void main(String[] args) throws ReflectiveOperationException {
        for (int i = 0; ; i++) {
            new CrashedFinalizable();
            // other code
        }
    }

    @Override
    protected void finalize() {
        System.out.print("");
    }
}

finalize 메서드를 주목하면 해당 메서드는 오직 콘솔에 빈 문자열을 출력합니다. 만약 finalize 메서드가 오버라이드되지 않았다면 JVM은 CrashedFinalizable 인스턴스를 finalizer를 가지고 있지 않은 인스턴스로써 취급했을 것입니다. 따라서 이 경우 거의 아무것도 하지 않은 finalize()를 제공해야 합니다.

 

main 메서드 안에서 CrashedFinalizable 인스턴스가 매 반복마다 생성됩니다. 이 인스턴스는 참조 변수에 할당되지 않으므로 가비지 컬렉션의 대상이 되기 충분합니다.

 

other code에 실행중에 메모리에서 얼마나 많은 인스턴스가 존재하는지 확인해보겠습니다.

public class CrashedFinalizable {
    public static void main(String[] args) throws ReflectiveOperationException, InterruptedException {
        for (int i = 0; ; i++) {
            new CrashedFinalizable();
            
            if ((i % 1_000_000) == 0) {
                Class<?> finalizerClass = Class.forName("java.lang.ref.Finalizer");
                Field queueStaticField = finalizerClass.getDeclaredField("queue"); // Finalizer 클래스에 queue 필드멤버를 가져옴
                queueStaticField.setAccessible(true); // queue에 접근허용
                ReferenceQueue<Object> referenceQueue = (ReferenceQueue) queueStaticField.get(null);

                Field queueLengthField = ReferenceQueue.class.getDeclaredField("queueLength"); // 큐의 길이 필드멤버 가져옴
                queueLengthField.setAccessible(true); // 멤버 접근 허용
                long queueLength = (long) queueLengthField.get(referenceQueue); // 삭제 대기중인 인스턴스의 개수를 참조
                System.out.format("There are %d references in the queue%n", queueLength);
            }
        }
    }

    @Override
    protected void finalize() {
        System.out.print("");
    }
}
실행결과
There are 0 references in the queue
There are 0 references in the queue
There are 0 references in the queue
There are 0 references in the queue
There are 0 references in the queue
There are 0 references in the queue
There are 2874828 references in the queue
There are 12195477 references in the queue
There are 10321743 references in the queue
There are 9322769 references in the queue
There are 8412356 references in the queue
There are 8010178 references in the queue
There are 7056920 references in the queue
There are 5946122 references in the queue
...

위 실행결과를 보면 처음에는 큐안에 인스턴스들이 없다가 시간이 지날수록 큐에 인스턴스의 개수가 쌓이는 것을 볼 수 있습니다. 위 실행결과를 통해서 가비지 컬렉터가 인스턴스 삭제를 잘 하지 못하는 것을 알 수 있습니다. 이렇게 큐에 삭제 대기 중인 인스턴스가 늘어날수록 시스템 충돌이 발생하여 메모리 부족 에러가 발생할 수 있습니다. 만약 우리가 finalizer 메서드를 제거한다면 참조의 개수는 보통 0일것이고 프로그램은 계속해서 수행될 것입니다.

 

2.3 finalizer를 사용하면 왜 OutOfMemoryError가 발생하는가?

 finalizer를 가지고 있는 인스턴스 A를 생성할때 JVM은 java.lang.ref.Finalizer 타입의 인스턴스를 함께 생성합니다. 다 사용한 인스턴스 A가 가비지 컬렉션 대상이 되면 JVM은 인스턴스 A를 처리할 준비가 되었다고 표시하고 참조 큐에 넣습니다.

 

한편 Finalizer 스레드는 계속 실행되며 참조 큐에서 삭제할 인스턴스를 탐색합니다. 삭제할 인스턴스를 탐색하면 큐에서 삭제할 인스턴스를 삭제(dequeue)하고 해당 인스턴스의 finalizer를 호출합니다.

 

다음 가비지 컬렉션이 실행되는 동안 삭제될 인스턴스가 더이상 어떤 참조 변수로부터 참조되지 않으면 참조가 삭제될 것입니다.

 

만약 한 스레드가 고속으로 인스턴스 생성을 계속한다면(위 예제와 같은 경우) Finalizer 스레드는 따라가지 못할 것입니다. 결국 메모리가 모든 객체를 저장할 수 없게 되고 OutOfMemoryError가 발생합니다. 위와 같은 예제를 통하여 알 수 있는 점은 인스턴스가 고속의 속도로 생성되는 상황은 자주 발생하지는 않지만 finalizer가 수행되는데 메모리 자원을 많이 사용한다는 점을 증명한다는 것을 알 수 있습니다.

3. Finalizer를 사용하지 않는 예제

finalize() 메서드를 사용하지 않고 동일한 기능을 제공하는 솔루션을 살펴보겠습니다.  다음 예제가 finalizer를 대체할 수 있는 유일한 방법은 아닙니다. 대신 이 방법은 중요한 점을 입증하는데 사용됩니다. 즉, finalizer를 회피하는데 도움이 되는 옵션이 항상 있습니다. 

public class CloseableResource implements AutoCloseable {
    private BufferedReader reader;

    public CloseableResource() {
        InputStream input = CloseableResource.class.getResourceAsStream("file.txt");
        reader = new BufferedReader(new InputStreamReader(input));
    }

    public String readFirstLine() throws IOException {
        String firstLine = reader.readLine();
        return firstLine;
    }

    @Override
    public void close() {
        try {
            reader.close();
            System.out.println("Closed BufferedReader in the close method");
        } catch (IOException e) {
            // handle exception
        }
    }
}

새로운 CloseableResource 클래스와 이전 Finalizable 클래스의 유일한 차이점은 Finalizer 정의 대신 AutoCloseable 인터페이스를 구현했다는 것입니다.

 

다음은 입력 파일을 읽고 작업을 완료한 후 자원을 해제하는 테스트 방법입니다.

	@Test
	public void whenTryWResourcesExits_thenResourceClosed() throws IOException {
	    try (CloseableResource resource = new CloseableResource()) {
	        String firstLine = resource.readFirstLine();
	        assertEquals("baeldung.com", firstLine);
	    }
	}

위의 테스트에서 CloseableResource 인스턴스는 try-with-resources 문의 try block에 생성되므로 try-with-reousrces 블록 실행이 완료되면 해당 자원이 자동으로 닫힙니다. 위 테스트를 실행하면 CloseableResource 클래스의 close 메서드에서 출력된 메시지가 표시됩니다.

 

5. 결론

이 글에서는 Java의 핵심 개념인 finalize 메서드를 봤습니다. finalize는 글상으로는 유용해보이지만 실행 시 좋지 않은 부작용을 일으킬 수 있습니다. 그리고 더 중요한 것은, finalizer를 사용하는 것에 대한 대안적인 솔루션이 항상 있다는 것입니다. 한가지 중요한 점은 finalizer 작업이 Java 9부터 더 이상 사용되지 않으면 결국 제거될 것이라는 점입니다.

 

References

A Guide to the finalize Method in Java