본문 바로가기

Library/Android in Cafe Java

템플릿 메서드를 사용하여 자바의 리소스를 관리하라

자바가 등장한 초창기 무렵을 생각해 봤을 때, 자동 메모리 관리 기능은 메모리를 직접 관리해야 하는 고통에 시달리던 프로그래머들에게 구세주처럼 여겨졌다. 그러나, 자바가 점점 그 영역을 넓혀감에 따라, 자동 메모리 관리 기능은 좋기만 한 것이 아니라는 점 역시 드러나게 되었다. 파이널라이저(finalizer)의 실행 시점은 자바  VM 구현에 따라 천차만별이며, 가비지 컬렉터(garbage collector)가 실행될 때 모든 자바 스레드가 순간적으로 정지해야 한다는 점은 특정 영역에서 치명적인 단점이다.

 

특히, 예외 처리 및 일반화 프로그래밍과 맞물리면서, 안전하게 리소스를 관리하는 것은 쉽지 않은 일이 되었다. C++의 RAII(Resource Acquisition Is Initialization) 기법을 애용하고 있는 프로그래머라면, 자바의 파이널라이저는 절대로 C++의 소멸자와 비슷하게 사용할 수 없기 때문에 매우 고통스러울 것이다. 자바의 가비지 컬렉터의 성능이 예전처럼 처참한 것은 아니지만, 모든 것을 가비지 컬렉터에게 맡기는 것은 자바 프로그램의 성능을 어마어마하게 떨어뜨리는 가장 큰 요인이다. 가비지 컬렉터가 자동으로 더 이상 사용하지 않는 메모리 조각을 추적해서 회수한다고 하더라도, 사용이 끝난 개체는 정리 작업을 통해 더 이상 사용하지 않는 것을 분명하게 밝혀주는 것이 좋다.

 

서론이 길었는데, C++에서 RAII 기법에 의해 리소스를 기계적으로 관리하던 프로그래머라면 자바에서도 코드의 반복 없이, 리소스를 안전하게 기계적으로 관리하기를 원할 것이다. 이를 위해, 자바에서는 템플릿 메서드(Template Method) 패턴을 사용할 수 있다. 예를 들어, FileInputStream 개체를 사용해야 하고, 예외가 발생했을 때 FileInputStream 개체가 열려있었다면 메서드에서 빠져나가기 전에 반드시 close() 메서드를 호출해야 한다.

 

 

InputStream in = null;

try {

    in = new FileInputStream(fileName);

    // do what you want

    ...

    ...

} catch(FileNotFoundException e) {

    // exception handling

} catch(IOExceotion e) {

    // exception handling

} finally {

    if(null != in) in.close();

}

 

 

이것은 상당히 문제 상황을 단순화한 것이다. 실제 상황에서는 중첩된  try-catch-finally 구문을 사용해야 할 경우도 있으며, 특히, '예외는 가급적 빨리 던지고, 예외를 잡아내는 것은 최대한 뒤로 미뤄라'라는 원칙을 고려해본다면 실제 코드에서는 try-catch-finally 구문을 사용해서 예외를 잡고, 사용한 리소스를 정확하게 회수하는 것은 쉽지 않다. 예외 처리까지 동반한 리소스 회수 코드가 짧지 않다보니, 프로젝트 전체에 걸쳐 이와 같은 코드가 반복된다면 이것은 매우 문제가 된다. 이럴 때, 자바에서는 템플릿 메서드 패턴을 사용할 수 있다.

 

 

public class FileInputStreamTemplate {

    public void process(String fileName) {

        InputStream in = null;

        try {

            in = new FileInputStream(fileName);

            doProcess(in);

        } catch(FileNotFoundException e) {

            // exception handling

        } catch(IOException e) {

            // exception handling

         } finally {

            if(null != in) in.close();

        }

    }

 

    public absrtact void doProcess(InputStream in) throws IOException;

}

 

 

스트림을 처리할 루틴은 그때 그때 다르지만, 해당 스트림에 의해 발생할 수 있는 예외와 스트림 정리 작업은 어디서나 대체적으로 비슷하다. 그렇다면, 실제 스트림을 다루는 루틴은 사용자가 정의할 수 있도록 추상 함수로 정의하고, 나머지 과정은 미리 정해진 루틴에 의해 처리하면 반복되는 코드를 줄일 수 있다. 위의 코드는 템플릿 메서드 패턴을 가장 간단하게 구현한 것인데, 사용할 때는 FileInputStreamTemplate을 상속받아 doProcess 메서드를 재정의 한 클래스를 생성할 수도 있고, 익명 클래스를 사용하여 사용 시점에서 doProcess 메서드를 재정의 할 수도 있다.

 

 

new FileInputStream() {

    public void doProcess(InputStream in) throws IOException {

        int len = 0;

        byte[] buf = new byte[4096];

        while((len = in.read(buf)) > 0) {

            // do what you want

        }

}.process("test.txt");

 

 

가장 처음의 코드와 비교해 봤을 때, 리소스 정리 과정은 모두 FileInputStream 내부에서 처리되며, 사용자는 스트림 처리 루틴에 좀 더 집중 할 수 있다. doProcess() 메서드는 인터페이스로 정의할 수도 있고, FileInputStream 클래스를 생성하고 싶지 않다면 process() 메서드를 static으로 선언하는 방법도 고려할 수 있다.

 

여기서, FileInputStream 외에도 FileOutputStream이나 BufferedInputStream과 같은 다른 개체에 대해서, 일반화 프로그래밍을 사용하여 반복되는 코드를 최대한 줄이고자 하는 사용자도 있을 것이다. 그러나, 자바는 임의의 타입 T를 직접 인스턴스화 할 수 없기 때문에 이것은 불가능하다. 자바는 일반화 프로그래밍보다 OOP 해결법을 더 강조하기 때문에, 리소스 개체에 따라 메서드를 오버라이드하거나, 전략(strategy) 패턴도 함께 응용하는 구현이 훨씬 자바와 더 잘 어울린다.