IT_Architecture/Design Pattern

Resource Release Patterns

JJun ™ 2014. 3. 31. 14:06

 


 출처: http://aeternum.egloos.com/1517782


 

시스템 리소스의 사용이 완료된 후 해제 되었음을 보장하기 위해서는 어떻게 해야 하는가?

 

시스템의 자원은 유한하다. 동시에 열 수 있는 파일의 수, 스레드의 최대 수, 데이터베이스 컨넥션 풀의 개수 등 동시에 사용되는 모든 자원은 시스템 가용성에 영향을 미친다. 시스템으로부터 리소스를 할당 받은 후 정상적으로 해제하지 않을 경우 시스템의 엔트로피는 점점 증가하고 마침내는 시스템 생태계의 종말을 초래하고 말 것이다.


여기에서는 가장 일반적인 시스템 자원인 파일을 사용해서 리소스를 해제하는 몇 가지 방법을 살펴 보기로 한다.

 

finally 블록(Java)

객체 지향 언어에서는 일반적으로 비정상적인 문제가 발생한 경우 이 사실을 통지하기 위해 예외Exception 메커니즘을 사용한다. 예외는 C 언어에서의 반환 값 전달 방식에 비해 코드의 가독성이 높아지고 코드 상의 정상 경로와 비정상 경로를 분리할 수 있다는 장점을 가지지만 실행 경로 상의 특정 지점에서 다른 지점으로 분기가 가능하기 때문에 오용할 경우 goto와 유사한 문제점이 발생할 수 있다.


특정 지점에서 예외가 발생할 경우 나머지 코드가 실행되지 않고 호출 지점으로 분기되기 때문에 할당 받은 리소스를 정상적으로 해제하지 못 하는 문제가 발생한다. 다음은 텍스트 파일을 읽어 한 라인씩 출력하는 코드이다.

 

public void readEmployees() {

BufferedReader reader = null;

            

try {

String line = null;

       reader = new BufferedReader(new FileReader("Employees.txt"));

       while((line = reader.readLine()) != null) {

           System.out.println(line);           

       }

       reader.close();

}catch(IOException ex) {

       throw new EmployeeDataReadException(ex);

}

}

 

위 코드의 문제는 reader.readLine()에서 예외가 발생할 경우 reader.close() 문이 실행되지 않아 리소스 누수가 발생할 수 있다는 점이다. 해결 방법은 리소스 해제 구문인 reader.close()를 finally 블록 내에 위치 시켜 예외 상황에서도 리소스가 해제됨을 보장하는 것이다.

 

public void readEmployees() {

BufferedReader reader = null;

            

try {

String line = null;

       reader = new BufferedReader(new FileReader("Employees.txt"));

       while((line = reader.readLine()) != null) {

           System.out.println(line);           

       }                  

}catch(IOException ex) {

       throw new EmployeeDataReadException(ex);

} finally {

       if (reader != null) {

           try {

               reader.close();

           } catch(IOException ex) {

               log.warning("File Hadnle Close Error!");

           }

       }

}

}

 

using 구문(C#)

try ~ finally 블록의 문제점은 실제 목적인 파일을 읽기 위한 구문보다 예외나 리소스 해제를 위해 필요한 구문의 비중이 더 크다는 점이다. 또한 try 구문 전에 readernull로 설정해야 하는 번거로움이 있고 finally 구문이 종료된 후에도 reader가 계속 존재하기 때문에 실수로 리소스를 사용하려고 할 경우 또 다른 예외가 발생할 수 있는 가능성이 존재한다.


C#에서는 try ~ finally의 번거로움을 해결하기 위해 using 구문을 제공한다. using 구문은 IDisposal 인터페이스를 구현한 객체에 대해 리소스 해제를 처리하는 dispose() 메소드의 호출을 보장해 준다. 따라서 finally 블록과 동일하게 리소스 해제를 보장 받으면서도 코드를 깔끔하게 유지할 수 있다.

 

public void ReadEmployees()

{

using(TextReader reader = new StreamReader("Employees.txt"))

    {

        string line = null;

        while ((line = reader.ReadLine()) != null)

        {

            Console.WriteLine(line);

        }

}

}

 

Callback(Java)

try ~ finally using 구문의 경우 리소스를 사용하는 모든 부분에 리소스를 할당하거나 해제하는 코드를 중복시켜야 한다. 변경되는 부분과 변경되지 않는 부분을 분리하는 것은 소프트웨어 설계의 기본 원칙이다. DRY 원칙에 따라 리소스 할당/해제 부분을 리소스를 사용해서 실제 작업을 처리하는 부분과 분리하자.


파일에서 읽어 들인 라인을 사용해서 작업을 처리하는 부분부터 작성하자. 실제 처리할 작업 내용은 프로그램마다 다르기 때문에 인터페이스로 정의함으로써 확장 가능하도록 만든다.

 

public interface LineCallback {

void process(String line) throws Exception;

}

 

파일을 열고 닫는 작업을 처리하는 FileTemplate 클래스를 추가하고 파일을 한 줄씩 읽어 LineCallback process() 오퍼레이션에 전달한다.

 

public class FileTemplate {

private static final Logger log

    Logger.getLogger("FileReaderSample");

 

public void read(String path, LineCallback callback)

throws FileReadException {

BufferedReader reader = null;

       try {

           reader = new BufferedReader(new FileReader(path));

                   

           String line = null;

           while((line = reader.readLine()) != null) {

              callback.process(line);

           }

       }catch(IOException ex) {

           throw new FileReadException(ex);

       } catch (Exception ex) {

           throw new FileReadException(ex);

       } finally{

           if (reader != null) {

               try {

                 reader.close();

             } catch(IOException ex) {

                 log.warning("File Hadnle Close Error!");

             }

           }

       }

}

}

 

FileTemplate를 사용함으로써 프로그래머들은 파일을 처리하기 위한 메커니즘과 무관하게 실제 처리 로직에만 집중할 수 있다.

 

new FileTemplate().read(path,

new LineCallback() {

       public void process(String line) throws Exception {

           System.out.println(line);

       }

});

 

Callback STRATEGY 패턴의 변형으로 Spring 프레임워크의 경우 인프라스트럭처와 관련된 JdbcTemplate이나 JmsTemplate 등의 클래스에서 광범위하게 사용되고 있다.

 

Closure(Ruby)

Java에서 Callback을 사용하는 이유는 함수가 언어의 제 1 시민first-class citizen이 아니기 때문이다. Java에서 파라미터로 전달할 수 있는 것은 객체뿐이므로 함수를 파라미터로 전달하는 효과를 얻기 위해 인터페이스와 클래스를 사용할 수 밖에 없다.


그러나 Ruby와 같이 함수가 제 1 시민인 언어에서는 간단하게 함수를 전달할 수 있다. 앞에서 살펴본 파일을 열고 닫는 등의 파일에 대한 처리는 블록 block을 인자로 받는 File.open() 메소드를 사용해서 해결할 수 있다.

 

class FileTemplate

  def read(path)

    File.open(path) do |f|

      f.each_line do |line|

        yield line

      end

    end

end

end

 

이제 라인을 한 줄씩 출력하는 블록을 read()로 전달하기만 하면 된다.

 

read(path) {|line| puts "#{line}"}

 

Ruby의 블록은 JavaCallback과 동일한 처리를 하면서도 더 적은 코드로 더 간결하고 깔끔한 코드를 작성할 수 있도록 한다. 이것은 Closure를 지원하는 Ruby, Smalltalk, C# 등의 언어로 작성된 코드가 그렇지 못한 언어로 작성된 코드에 비해 가독성이 뛰어나면서도 에러가 적은 이유이기도 하다.