본문 바로가기
이펙티브 자바

[아이템 9] try-finally 보다는 try-with-resource를 사용하라

by 에드박 2021. 4. 25.

자바의 라이브러리에는 close 메서드를 호출해서 직접 닫아줘야 하는 자원이 많습니다.

직접 닫아줘야 하는 예로 InputStream, OutputStream, java.sql.Connection 등등이 있습니다.

 

자원 닫기는 라이브러리를 사용하는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어질 수 있습니다.

이런 자원 중 상당수가 finalizer 를 활용해서 안전망을 구현했지만 아이템8 을 보면 알 수 있듯

finalizer는 믿을만하지 못합니다.

 

이전에 자원이 제대로 닫힘을 보장하는 수단으로는 주로 try-finally가 쓰였습니다.

예외가 발생하거나 메서드에서 반환되는 경우를 포함해서 말입니다.

아래의 예제는 try-finally를 사용해서 자원을 해제하는 예제입니다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryFinallyEx1 {

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

 

위의 예제는 나쁘지 않습니다. 닫아야 할 자원이 단 하나만 존재하기 때문입니다.

하지만 위의 예제에서는 try 문과 finally 문에서 모두 예외가 발생할 수 있습니다.

만약 물리적인 문제로 인해 제대로 동작할 수 없는 상태가 된다면 readLine() 메서드가 예외를 던지고, 같은 이유로 close() 메서드도 실패할 것입니다. 이런 상황이라면 두번째 예외가 첫번째 예외를 집어삼켜버립니다.

따라서 스택추적 내역에는 첫번째 예외에 대한 정보는 남아있지 않게 됩니다.

-> 디버깅을 몹시 어렵게 만듭니다.

이에대한 대응으로 아래와 같은 코드를 만들 수 있지만 코드가 매우 지저분해집니다.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class TryFinallyEx1 {

    static String firstLineOfFile(String path) throws IOException {
        BufferedReader br = new BufferedReader(new FileReader(path));
        try {
            return br.readLine();
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

만약 닫아야 할 자원이 2개가 된다면 어떨까요?

 

import java.io.*;

public class TryFinallyEx2 {
    private static final int BUFFER_SIZE = 10;

    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 {
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

 

위와 같이 엄청 지저분한 코드가 될것입니다.

이러한 문제들은 자바7 부터 등장한 try-with-resource덕에 모두 해결되었습니다.

 


try-with-resource

try-with-resource 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야 합니다.

단순히 void를 반환하는 close 메서드 하나만 덩그러니 정의한 인터페이스 입니다.

 

자바의 AutoCloseable 인터페이스

 

자바 라이브러리와 서드파티 라이브러리들의 수많은 클래스와 인터페이스가 이미 AutoCloseable을 구현하거나 확장해뒀습니다.

만약 닫아야하는 자원을 뜻하는 클래스를 작성한다면 AutoCloseable을 반드시 구현하는것이 좋습니다.

 

public class MyCloseableClass implements AutoCloseable{

    @Override
    public void close() throws Exception { }
}

 

 

다음은 위에서 try-finally로 자원해제를 구현했던 코드를 try-with-resource로 변경한 코드입니다.

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

 

 

public class TryWithResourceEx2 {
    private static final int BUFFER_SIZE = 10;

    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);
            }
        }
    }
}

 

try-with-resource 버전이 코드도 엄청나게 짧아지고 읽기도 편해졌습니다.

 

이제 firstLineOfFile 메서드에서 readLine() 과 close() 메서드(현재는 나타나 있지 않은 close) 양쪽에서 예외가 발생하면 cloase 에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록됩니다. 그리고 숨겨진 예외는 '숨겨졌다(suppressed)'라는 꼬리표를 달고 출력 됩니다. 이것은 자바7에서 Throwable에 추가된 getSuppressed() 메서드를 사용해서 가져올 수 있습니다.

 

그리고 try-with-resource 문에서도 일반적인 try-catch문 처럼 catch블럭과 finally블럭을 사용할 수 있습니다.

 

꼭 회수해야 하는 자원을 다룰 때는 try-finally 대신 try-resource를 사용하는 것이 장점이 많습니다.

  • 코드가 짧아진다.
  • 만들어지는 예외 정보도 훨씬 유용하다.
  • 손쉽게 자원 해제가 가능하다.

댓글