EffectiveJava 의 아이템 9. try-finally 보다는 try-with-resource를 사용하라
를 읽고 내가 작성한 코드에 문제점이 있음을 인지했다.
InputStream
, OutputStream
, java.sql.Connection
과 같이 사용이 끝나면 close
메서드를 호출해 자원을 회수해야 하는 자원들이 있다. 현재 내가 진행중인 프로젝트에서는 apache POI
API를 사용해 DB의 데이터들을 핸들링해 excel 파일을 만들고 있고, 이 때 다루는 Workbook
인터페이스를 구현한 클래스들은 마찬가지로 사용이 끝나면, close
메서드를 통해 자원을 회수해야 한다. 자원을 회수하지 않는다면 프로그램의 성능에 문제가 생길 것이다.
만약, 클라이언트가 명시적으로 자원을 회수하지 않는다면 이 자원들은 finalizer
로 자원이 회수 될 수도 있다. 하지만 첫 번째로 finalizer
가 구현이 되어 있어야 하고 두 번째로 말 그대로 회수 '될 수도' 있는 것이지 회수 되는 것이 '보장'되지는 않는다.
때문에 close
메서드를 명시적으로 호출하는 방법으로 자원을 회수해야 하고, try-finally
와 함께 close
메서드를 호출하는 것이 일반적이였으나, 자바7 이후로는 try-with-resource
를 통해 위와 같은 문제를 해결할 수 있다.
먼저 EffectiveJava 서적에서 제시하는 전통적인 방법의 try-finally
예시 코드다.
static String readLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
분명히 나쁜 코드라는 느낌은 들지 않지만, close
가 필요한 자원이 두개 이상이 된다면,
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();
}
}
위와 같이 복잡한 코드가 작성된다. 또한 try
, finally
블록 모두에서 예외가 발생할 가능성이 있는데 물리적인 문제로 두 블록에서 모두 예외가 발생한다면 두 번째 예외가 첫 번째 예외를 집어삼켜버려 스택을 추척해도 첫 번째 예외에 관한 정보가 남지 않는다는 문제점이 있다.
try-with-resource
는 try-finally
의 문제점을 모두 해소한다. 심지어 코드가 훨씬 간결해진다. 하지만 그 전에 AutoCloseable
인터페이스를 implements 하는 것이 조건으로 주어진다.
public interface AutoCloseable {
void close() throws Exception;
}
AutoCloseable
인터페이스는 위의 코드가 전부다. InputStream
, Workbook
등의 클래스에서 모두 이 인터페이스를 구현하고 있다. (Closeable
은 AutoCloseable
을 상속한 인터페이스로 close
메서드의 throws Exception
부분이 throws IOException
인 부분을 제외하면 동일하다.)
public abstract class InputStream implements Closeable {
...
}
public interface Workbook extends Closeable, Iterable<Sheet> {
...
}
마찬가지로 서적에서 제시하는 try-with-resource
방식의 코드이다. close
메서드의 호출을 코드에서 명시적으로 하고 있지는 않지만, try
블록의 return 이후에 ()
안의 자원이 AutoCloseable
을 구현했다면 자동적으로 close
가 호출되는 것이다.
static String readLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
자원 회수가 필요한 자원이 두개 이상인 경우에는
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);
}
}
}
이와 같이 자원의 선언 사이에 ;
으로 구분한다.
public class ExcelClient {
public File createMonthlyDocumentFile(int year, int month) {
Workbook workbook = new SXSSFWorkbook();
CellStyle style = createCellStyle(workbook);
...
}
return exportWorkbook(workbook);
}
private File exportWorkbook(Workbook workbook) {
try {
File file = File.createTempFile(UUID.randomUUID().toString(), "xlsx");
FileOutputStream fileOutputStream = new FileOutputStream(file);
workbook.write(fileOutputStream);
workbook.close();
return file;
} catch (IOException exception) {
throw new RuntimeException("파일 생성 실패");
}
}
}
프로젝트 코드에서 workbook
자원은 exportWorkbook
메서드에서 명시적으로 호출을 하여 자원을 회수하고 있지만, 지금 보니 exportWorkbook
메서드에서는 fileOutputStream
의 자원을 회수하지 않고 있다.
workbook
의 자원은 createMonthlyDocumentFile
메서드에서 회수를 하도록 하고, 마찬가지로 fileOutputStream
의 자원은 해당 자원이 사용되는 exportWorkbook
메서드에서 회수를 하도록 코드를 변경해야한다.
public class ExcelClient {
public File createMonthlyDocumentFile(int year, int month) {
try (Workbook workbook = new SXSSFWorkbook()) {
CellStyle style = createCellStyle(workbook);
...
return exportWorkbook(workbook);
}
}
private File exportWorkbook(Workbook workbook) throws IOException {
File file = File.createTempFile(UUID.randomUUID().toString(), "xlsx");
try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
workbook.write(fileOutputStream);
return file;
}
}
}
workbook
과 fileOutputStream
을 try-with-resource
방식을 통해 자원이 회수되도록 코드를 변경했다. (두 객체 모두 AutoCloseable
인터페이스를 구현한 클래스로 생성됐다.) 이 외에도 코드에 수정된 부분이 있지만 주제와 관련된 부분만 작성했다.