JAVA/Effective Java

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

꾸준함. 2024. 1. 24. 07:29

Java 7부터 try-with-resources 구문이 등장해 try-finally는 더 이상 최선의 방법이 아닙니다.
try-with-resources를 사용할 경우 장점은 아래와 같습니다.

  • 코드가 훨씬 간결해져 가독성이 좋아짐
  • 예외가 두 개 이상 발생했을 때 예외를 잡아먹지 않기 때문에 전부 확인 가능

 

코드가 훨씬 간결해져 가독성이 좋아짐

자원이 하나인 경우 try-finally 절도 괜찮아 보이지만 자원이 두 개 이상이 되어 try-finally 절이 중첩이 되는 순간부터 가독성이 급격하게 안 좋아집니다.

 
단일 자원

 

// 단일 자원 try-finally
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
// 단일 자원 try-with-resources
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
view raw .java hosted with ❤ by GitHub

 

 
다중 자원

 

// 다중 자원 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();
}
}
// 다중 자원 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);
}
}
}
view raw .java hosted with ❤ by GitHub

 
다중 자원 코드를 보고 혹여나 아래와 같이 코드를 작성하면 단일 try-finally 절로 작성이 되지 않을까?라고 생각할 수 있겠지만 이렇게 작성할 경우 finally 절 내 out.close()에서 예외가 발생할 경우 in.close()가 실행도 되지 않을 수 있는 문제점이 있습니다.
 

static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
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();
in.close();
}
}
view raw .java hosted with ❤ by GitHub

 
 
적어도 반납할 자원에 대한 close() 메서드가 정상적으로 호출됐다는 것을 보장하기 위해서는 중첩 구조로 코드로 작성할 수밖에 없습니다.
비슷한 예시로 저자가 본인이 작성한 Java Puzzler 책을 언급했는데 코드는 아래와 같습니다.
 

static void copy(String src, String dst) throws IOException {
InputStream in = new FileInputStream(src);
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 {
try {
out.close();
} catch (IOException e) {
// 이렇게 하면 되는거 아닌가?
/**
* IOException이 아니라 RuntimeException이 발생한다면?
* in.close() X
*/
}
try {
in.close();
} catch (IOException e) {
// 안전한가?
}
}
}
view raw .java hosted with ❤ by GitHub

 
 
해당 코드 또한 out.close()에서 IOException이 아닌 RuntimeException 발생 시 in.close()가 대처를 못하여 in.close()가 호출도 안된다는 문제를 야기하는 코드입니다.
이처럼 try-finally는 가독성뿐만 아니라 코드 안정성 측면에서 열위를 보입니다.
반면 try-with-resources 절 도입 시 중첩 구조를 피할 수 있고 가독성 좋은 코드를 작성할 수 있을 뿐더러 target 폴더 내 생성된 byte code를 보면 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[8192];
int n;
while((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} catch (Throwable var8) {
try {
out.close();
} catch (Throwable var7) {
var8.addSuppressed(var7);
}
throw var8;
}
out.close();
} catch (Throwable var9) {
try {
in.close();
} catch (Throwable var6) {
var9.addSuppressed(var6);
}
throw var9;
}
in.close();
}
view raw .java hosted with ❤ by GitHub

 

 

예외가 두 개 이상 발생했을 때 예외를 잡아먹지 않기 때문에 전부 확인 가능

try-with-resources 절의 또 다른 장점은 예외를 잡아먹지 않는다는 것입니다.
try-finally 절이 중첩되어 있을 경우 마지막 예외만 터미널에 찍히는 것을 확인할 수 있습니다.
 

public class BadBufferedReader extends BufferedReader {
public BadBufferedReader(Reader in, int sz) {
super(in, sz);
}
public BadBufferedReader(Reader in) {
super(in);
}
@Override
public String readLine() throws IOException {
throw new CharConversionException();
}
@Override
public void close() throws IOException {
throw new StreamCorruptedException();
}
}
public class TopLine {
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BadBufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
br.close();
}
}
public static void main(String[] args) throws IOException {
System.out.println(firstLineOfFile("pom.xml"));
}
}
view raw .java hosted with ❤ by GitHub
마지막 예외만 출력

 
 
반면, try-with-resources를 적용 시 모든 예외가 찍히는 것을 확인할 수 있습니다.
사실 장애 원인 분석을 할 때 마지막 예외보다는 최초로 발생한 예외가 더 중요하기 때문에 try-with-resources를 권장합니다.
 

public class TopLine {
static String firstLineOfFile(String path) throws IOException {
try (BufferedReader br = new BadBufferedReader(new FileReader(path))) {
return br.readLine();
}
}
public static void main(String[] args) throws IOException {
System.out.println(firstLineOfFile("pom.xml"));
}
}
view raw .java hosted with ❤ by GitHub
모든 예외 출력

 
 
try-with-resources가 모든 예외를 출력하는 이유는 target 폴더 내 바이트 코드를 확인하면 알 수 있습니다.
바이트 코드를 보면 예외가 발생할 때마다 잡아서 addSuppressed 메서드를 호출하는 것을 확인할 수 있고 해당 메서드 덕분에 마지막 예외가 다른 예외를 잡아먹지 않고 전부 출력할 수 있습니다.


public class TopLine {
public TopLine() {
}
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BadBufferedReader(new FileReader(path));
String var2;
try {
var2 = br.readLine();
} catch (Throwable var5) {
try {
br.close();
} catch (Throwable var4) {
var5.addSuppressed(var4);
}
throw var5;
}
br.close();
return var2;
}
public static void main(String[] args) throws IOException {
System.out.println(firstLineOfFile("pom.xml"));
}
}
view raw .java hosted with ❤ by GitHub

 

정리

try-finally보다 try-with-resources가 가독성 측면 그리고 안전성 측면에서 모두 좋으므로 try-finally 절이 보인다면 모두 try-with-resources로 변경하는 것을 권장합니다.
 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 1부 - 백기선 강사님
 

반응형