자바 라이브러리에는 close 메서드를 호출해 직접 닫아줘야하는 자원이 많다. InputStream, OutputStream, java.sql.Connection 등이 그렇다. 상당 수가 finalizaer를 통해 이를 예방하려고 하지만 그리 믿을만하진 못하다.
전통적으로 이를 해결할 때 try-finally를 썼다.예외가 발생하거나 메서드에서 반환되는 경우를 포함해서 말이다.
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try{
return br.readLine();
}finally{
br.close();
}
}
만약 여기서 자원을 더 사용하면 코드가 너무 복잡해진다.
static String 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 블록에서 생길 수 있는 문제점들이 있다. 예를들어, 기기에 문제가 생겨 firstLineOfFile 메서드 안에 readLine 메서드가 예외를 던지면 close메서드도 실패할 것이다. 이러면 두 번째 예외가 첫 번째 예외를 완전히 집어 삼키게된다. 그러면 스택 추적 내역에 첫 번째 예외에 관한 정보는 남지 않게되어 실제 시스템에서의 디버깅이 어려워지게된다.
try-with-resources는 try 객체를 전달하고try 코드 블록이 끝나면 자동으로 자원을 종료해준다. 그래서 꼭 회수해야하는 자원을 다룰 때는 try-with-resources를 다루는 것이 좋다. 그렇다면 이를 이용해 이를 해결해보자. 이 구조를 사용하려면 AutoCloseable
인터페이즈를 구현해야한다. try-with-resources에서 try 블록에 전달할 수 있는 자원은 AutoCloseable 인터페이스의 구현체로 한정되기 때문이다. 이는 단순히 void를 반환하는 close 메서드 하나만 덩그러니 정의한 인터페이스다.
public interface AutoCloseable {
void close() throws Exception;
}
이 인터페이스를 재정의하여 사용하고 자원을 반납해야하는 MyResource를 만들었다.
public class MyResource implements AutoCloseable {
public void run() {
System.out.println("프로그램 실행");
throw new RuntimeException("강제종료");
}
@Override
public void close() {
System.out.println("프로그램 종료하는 중");
throw new RuntimeException("종료하는 도중 문제발생");
}
}
public static void main(String[] args) {
try(MyResource myResource1 = new MyResource();
MyResource myResource2 = new MyResource()) {
myResource1.run();
myResource2.run();
}
}
// 첫 번째 코드를 try-wirh-resources 적용한 모습
static String firstLineOfFile(String path) throws IOException {
try(BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
// 두 번째 코드를 try-with-resources 적용한 모습
static String 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);
}
}
만약 readLine과 close 양쪽에서 예외가 발생하면 close에서 발생한 예외는 숨겨지고 readLine에서 발생한 예외가 기록된다. 이처럼 실전에서는 프로그래머에게 보여줄 예외 하나만 보존되고 여러 개의 다른 예외가 숨겨질 수도 있다. 이렇게 숨겨진 예외들도 그냥 버려지지는 않고 스택 추적내역에 숨겨졌다(suppressed)
라는 꼬리표를 달고 출력된다.
여튼 try-with-resources를 사용하면 try문을 중첩하지 않고도 다수의 예외를 처리할 수 있다. 또, 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;
}
}