InputStream,OutputStream,java.sql.Connection
등은 close 메서드를 호출해 직접 닫아줘야 한다. 전통적으로 우리는 try-finally를 사용해 닫힘을 보장했다.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TopLine {
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
}
어떤가? 평소에 자주 사용하던 방식이다. 이때 자원을 하나 더 사용해보자.
import java.io.*;
public class Copy {
private static final int BUFFER_SIZE = 8 * 1024;
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();
}
}
public static void main(String[] args) throws IOException {
String src = args[0];
String dst = args[1];
copy(src, dst);
}
}
자원이 하나 더 늘으니 try-finally 문 안에 또 try-finally 문이 들어가 굉장히 지저분해졌다. 하지만 위의 코드는 결점이 있는 코드이다. 예외는 try, finally블록 모두에서 발생할 수 있다.
다시 아래의 코드를 살펴보자.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TopLine {
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
}
만약 기기에 물리적 문제가 생긴다면 readLine()
에서 예외를 던질 것이고, 같은 이유로 close 메서드도 실패할 것이다. 이런 상황이라면 두번째 예외가 첫 번째 예외를 완전히 집어 삼켜 디버깅이 몹시 어렵다. 왜냐하면 두번째 예외가 첫번째 예외를 집어 삼켰으므로 스택 추적 내역에 첫 번째 예외에 관한 정보가 없어져 최초 발생 예외에 대해 알지 못하기 때문이다.
이는 try-with-resource를 통해 해결할 수 있다.
이 구조를 사용하려면 해당 자원이 AutoCloseable 인터페이스를 구현해야한다. 다음은 위의 코드를 try-with-resource를 사용해 재작성한 것이다.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TopLine {
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}
}
이전처럼 readLine
과 close
호출 양쪽에서 예외가 발생하면 close
에서 발생한 예외는 숨겨지고, readLine
에서 발생한 예외가 기록된다.
import java.io.*;
public class Copy {
private static final int BUFFER_SIZE = 8 * 1024;
// try-with-resources on multiple resources - short and sweet (Page 35)
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에서도 catch절을 쓸 수 있다.
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
public class TopLineWithDefault {
static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
}
꼭 회수해야 하는 자원을 다룰 때는 try-finally 말고, try-with-resources를 사용하자. 예외는 없다. 코드는 더 짧고 분명해지고, 만들어지는 예외 정보도 훨씬 유용하다. tyr-finally로 작성하면 실용적이지 못할 만큼 코드가 지저분해지는 경우라도, try-with-resources로는 정확하고 쉽게 자원을 회수할 수 있다.
public class FirstError extends RuntimeException {
}
public class SecondError extends RuntimeException {
}
위와 같이 두가지 에러가 존재한다고 하자.
public class MyResource implements AutoCloseable {
public void doSomething() throws FirstError {
System.out.println("doing something");
throw new FirstError();
}
@Override
public void close() throws SecondError {
System.out.println("clean my resource");
throw new SecondError();
}
}
그리고 MyResource에서 위에서 살펴보았던 예제와 같이 어떤일을 행하는 메서드와, 자원을 닫는 close 메서드 둘 다에서 error가 발생한다고 해보자.
MyResource myResource = new MyResource();
myResource.doSomething();
myResource.close();
그렇다면 위와 같이 사용하려고 코드를 작성할 수 있다. 하지만, 이렇게 작성하면 안된다. doSomething에서 에러가 나면 close가 되지 않고 즉, 정리가 되지 않고 끝나기 때문이다. 따라서 고전적인 방법으로 다음과 같이 작성할 수 있다.
MyResource myResource = new MyResource();
try{
myResource.doSomething();
} finally {
myResource.close();
}
하지만 위 코드에는 문제가 있다. 우리는 보통 에러가 났을 때 최초의 에러 원인을 알고자한다. 하지만 이를 실행하면 doSomething에서 에러가 나고, finally로 가 close에서 또 에러가 나므로 두번째 에러가 첫번째 에러를 잡아 먹게 된다. 따라서 결과가 아래와 같이 뜬다.
doing something
clean my resource
Exception in thread "main" SecondError
at MyResource.close(MyResource.java:11)
at Main.main(Main.java:7)
따라서 FirstError에 대해선 디버깅 하지 못한다. 그렇다면 try-with-resources를 사용해보자.
public class Main {
public static void main(String[] args) {
try (MyResource myResource = new MyResource()) {
myResource.doSomething();
}
}
}
코드가 간결해졌다. 그렇다면 결과는 어떨까?
doing something
clean my resource
Exception in thread "main" FirstError
at MyResource.doSomething(MyResource.java:5)
at Main.main(Main.java:4)
Suppressed: SecondError
at MyResource.close(MyResource.java:11)
at Main.main(Main.java:3)
최초의 원인인 FirstError에 대한 로그를 확인할 수 있다. 이로인해 디버깅이 훨씬 쉬워진다.
자원을 두개쓸 때도 훨씬 간결하다.
public class Main {
public static void main(String[] args) {
try (MyResource myResource = new MyResource(); MyResource myResource1 = new MyResource()) {
myResource.doSomething();
myResource1.doSomething();
}
}
}
결과는 아래와 같다.
doing something
clean my resource
clean my resource
Exception in thread "main" FirstError
at MyResource.doSomething(MyResource.java:5)
at Main.main(Main.java:4)
Suppressed: SecondError
at MyResource.close(MyResource.java:11)
at Main.main(Main.java:3)
Suppressed: SecondError
at MyResource.close(MyResource.java:11)
at Main.main(Main.java:3)
첫번째 doSomething에서 예외가 터졌지만 close는 둘 다 해주는 것을 볼 수 있다.
이펙티브 자바 3/E
[이팩티브 자바] #9 Try-with-Resource-백기선