1. 개념

자바 라이브러리에는 close 메서드를 직접 호출해서 닫아줘야 하는 자원이 많다. 대표적으로 InputStream, ouputStream java.sql.connection 등이 있으며 해당 자원들은 클라이언트에서 놓치기 쉬워 예측할 수 없는 성능 문제로 이어지곤 한다. 이중 상당 수가 finalizer를 안정망으로 사용하여 문제에 대비하고 있긴 하지만, 완전히 안전하다고 할 수 없다. (해당 내용은 다음 포스트에서 확인 가능)

[이펙티브 자바] - [이펙티브 자바] 8. finalizer와 cleaner 사용을 피하라

흔히 사용하는 try-finally를 사용한 예외처리를 확인해보자

2. try-finally

2-1. 자원을 1개 사용하는 try-finally 메서드

static String firstLineOfFile(String path) throws IOException    {
       BufferdReader br = new BufferedReader(new FileReader(Path));
       try {
           return br.readLine();
       }    finally {
           br.close();
       }
   }

 

2-2. 자원을 2개 사용하는 try-finally 메서드

자원을 1개 사용하는 경우는 꽤나 괜찮아 보인다, 그렇다면 자원을 여러 개 쓰는 경우 어떻게 될까?

static void copy(String src, String dst) throws IOException  {
        InputStream in = new FileInputStream(src);
        try {
            OutputStream out = new FileOutputStream(dst);
            try {
                byte[] buf = new byte[BUFFER_SIZE];
                int n;
                while ((n = in.read(buf)) >= 0)
                    out.write(buf, 0, n);
            }   finally {
                out.close();
            }
        } finally {
            in.close();
        }
   }

예제에서 볼 수 있듯, 2개 이상의 자원을 try-finally로 구현하면 너무 지저분해진다. 두 메서드 모두 올바르게 try-finally를 사용했지만 미묘한 결점이 있다. 바로 특정 예외가 숨겨질 수 있다는 점이다.  

2-3. try-finally의 결점

해당 메서드들은 try, finally 구문에서 모두 예외가 발생할 수 있다. 기기에 물리적 문제가 생기면 firstLineOfFile 메서드 안에 readLine 메서드가 예외를 던질 것이고, 그 이후 close 메서드도 실패하게 된다. 이 경우 두 번째 예외(close)가 첫 번째 예외(readLine)를 집어삼킨다. 그러면 stack traces에 첫 번째 예외는 정보가 남지않아서 실제 시스템에서 디버깅을 어렵게한다. 실제 운영 상황에서 문제를 해결하기 위해서는 첫번째 예외에 대한 로그를 보고 싶은 경우가 분명 있을 것이고,  두 번째 예외 대신 첫 번째 예외를 찍도록 코드를 수정할 수는 있지만, 코드가 너무 지저분해져서 그렇게 사용하는 경우는 거의 없다.

3. try-with-resources

이런 문제는 자바 7에서 try-with-resources로 모두 해결된다. 이 구조를 사용하면 해당 자원이 AutoCloseable인터페이스를 구현해야 한다. AutoCloseable 인터페이스를 확인해 보면 다음과 같이 close() 메서드만으로 이루어진 인터페이스이다.

구현된 AutoClosebale (정의된 close 메서드는) 리소스를 닫을 때 호출되며 명시적으로 닫아줄 필요 없이 자동으로 자원을 닫히게 해 준다.

try (ResourceType resource = acquireResource()) {
    // 리소스를 사용하는 코드
}

예를 들어 해당 코드에서 ResourceType은 AutoCloseable을 구현한 클래스이고, try 블록이 끝나면 자동으로 close() 메서드가 호출되며 리소스가 안전하게 닫힌다. 예외가 발생하더라도 리소스가 정확히 닫힘을 보장할 수 있다. 다양한 서드파티 라이브러리들도 AutoCloseable 인터페이스를 구현/확장하고 있어서 이 기능을 사용할 수 있다. 효율적인 자원 관리를 위해 닫아야 하는 자원을 대상으로 한다면 AutoCloseable을 꼭 구현해야 한다. 이제 위의 2개 메서드에 각각 try-with-resource를 적용해 보자

3-1. 자원을 1개 사용하는 try-with-resource 메서드

static String firstLineOfFile(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(
            new FileReader(path))) {
        return br.readLine();
    }
}

3-2. 자원을 2개 사용하는 try-with-resource 메서드

static void copy(String src, String dst) throws IOException  {
     try (InputStream in = new FileInputStream(src);
          OutputStream out = new FileOutputStream(dst))  {
         byte[] buf = new byte[BUFFER_SIZE];
         int n ;
         while ((n = in.read(buf)) >= 0)
             out.write(buf, 0, n);
     }
}

한눈에 봐도 가독성이 더 좋으며, 문제를 진단하기도 좋다. firstLineOfFiles 메서드를 먼저 확인해 보면, readLine, close에서 둘 다 에러가 발생할 시 기존의 try-finally 메서드와 다르게 숨겨진 예외(readLine)들도 버려지지 않고 stack trace에 숨겨졌다는(suppressed) 꼬리표를 달고 출력된다. 또한 자바 7의 Throwable에 추가된 getSuppressed 메서드를 사용하면 코드상에서도 쓸 수 있다.

3-3. try-with-resource와 catch 메서드

보통 try-finally처럼 try-with-resources에서도 catch절을 쓸 수 있다. catch을 함께 사용하면 try 문을 더 중첩하지 않고 다수의 예외처리가 가능하다. firstLineOfFile 메서드를 수정하여 예외가 발생했을 때 예외를 던 지는 대신 기본값을 반환하도록 수정하면 다음과 같다.

static String firstLineOfFile(String path, String defaultVal) {
        try (BufferedReader br = new BufferedReader(
                new FileReader(path))) {
            return br.readLine();
        } catch (IOException e) {
            return defaultVal;
        }
    }

4. 정리

꼭 회수해야 하는 자원을 다룰 때는 try-finally 가 아닌, try-with-resources를 사용하자. 예외적인 경우는 없으며 코드는 간결하고 분명해지고 예외정보를 추적하기에도 훨씬 좋다.  

내용 정리 및 테스트 코드

https://github.com/junhkang/effective-java-summary/tree/master/src/main/java/org/example/ch01/item08

https://github.com/junhkang/effective-java-summary/tree/master/src/main/java/org/example/ch01/item09