자바 라이브러리에는 close 메소드를 호출해 직접 닫아줘야 하는 자원이 많다. InputStream, OutputStream, java.sql.Connection 등이 좋은 예다. 자원 닫기는 클라이언트가 놓치기 쉬워서 예측할 수 없는 성능 문제로 이어지기도 한다.
예외가 발생하거나 메소드에서 반환되는 경우를 포함해, 전통적으로 자원이 제대로 닫힘을 보장하는 수단으로 try-finally가 쓰였다.
1 2 3 4 5 6 7 8 9 10 11 | /* * try-finally - 더 이상 자원을 회수하는 최선의 방책이 아니다! */ static String firstLineOfFile(String path) throws IOException { BufferedReader br = new BufferedReader(new FileReader(path)); try { return br.readLine(); } finally { br.close(); } } | cs |
나쁘지 않지만, 자원을 하나 더 사용한다면 어떨까?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* * 자원이 둘 이상이면 try-finally 방식은 너무 지저분하다! */ 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(); } } | cs |
try-finally 문을 제대로 사용한 앞의 두 코드 예제에는 미묘한 결점이 있다. try 블록이든 finally 블록이든, 기기에 물리적인 문제가 생긴다면 firstLineOfFile 메소드 안의 readLine 메소드가 예외를 던지고, 같은 이유로 close 메소드도 실패할 것이다. 이런 상황이라면 두 번째 예외가 첫 번째 예외를 집어삼켜, 스택 추적 내역에 첫 번째 예외에 대한 정보는 남지 않게 될 것이고, 실제 시스템에서의 디버깅을 몹시 어렵게 한다.
이러한 문제들은 자바 7이 투척한 try-with-resources 덕에 모두 해결되었다. 이 구조를 사용하려면 해당 자원이 반드시 AutoCloseable 인터페이스를 구현해야 한다. 다음의 try-with-resources를 사용해 위에 나온 try-finally의 첫 번째 예제 코드를 재작성한 예이다.
1 2 3 4 5 6 7 8 9 10 | /* * try-with-resources - 자원을 회수하는 최선책! */ static String firstLineOfFile(String path) throws IOException { try (BufferedReader br = new BufferedReader( new FileReader(path))) { return br.readLine(); } } | cs |
다음은 위에 나온 try-finally의 두 번째 예제 코드에 try-with-resources를 적용한 모습이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | /* * 복수의 자원을 처리하는 try-with-resources - 짧고 매혹적이다! */ 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); } } | cs |
try-with-resources 버전이 짧고 읽기 수월할 뿐만 아니라 문제를 진단하기도 훨씬 좋다. firstLineOfFile 메소드를 생각해보면, readLine과 close 호출 양쪽에서 예외가 발생하면, close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다. 이처럼 실전에서는 프로그래머에게 보여줄 예외 하나만 보존되고 여러 개의 다른 예외가 숨겨질 수도 있는데, 버려지지는 것이 아니라 스택 추적 내역에 '숨겨졌다(suppressed)'는 꼬리표를 달고 출력되며, 자바 7에서 Throwable에 추가된 getSuppressed 메소드를 이용하면 프로그램 코드에서 가져올 수도 있다.
try-with-resources에서도 catch 절을 쓸 수 있는데, 덕분에 try 문을 더 중첩하지 않고도 다수의 예외를 처리할 수 있다. 다음 코드는 firstLineOfFile 메소드를 살짝 수정하여 파일을 열거나 데이터를 읽지 못했을 때 예외를 던지는 대신 기본값을 반환하도록 해본 예이다.
1 2 3 4 5 6 7 8 9 10 11 12 | /* * try-with-resources를 catch 절과 함께 쓰는 모습 */ static String firstLineOfFile(String path, String defaultVal { try (BufferedReader br = new BufferedReader( new FileReader(path))) { return br.readLine(); } catch (IOException e) { return defaultVal; } } | cs |
꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자. 예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다. try-finally로 작성하면 실용적이지 못할 만큼 코드가 지저분해지는 경우라도, try-with-resources로는 정확하고 쉽게 자원을 회수할 수 있다.