자바에서 사용한 자원을 반납할 때는 try문의 finally에서 수동으로 직접 자원을 반납하는 방법 외에 try-with-resources구문을 사용하는 방법이 있습니다.
엄밀히 말해서 try-with-resources 자체가 사용한 자원을 알아서 반납해 주는건 아닙니다. try-wth-resources는 AutoCloseable 인터페이스의 정의된 close메서드를 실행해 줄 뿐입니다.
FileInputStream을 예시로 살펴보겠습니다. FileInputStream은 InputStream인터페이스를 구현한 구현체입니다.

InputStream인터페이스는 Closeable인터페이스를 상속받고 있습니다.

Closeable은 java.io패키지에 존재하는 인터페이스로 AutoCloseable을 상속받고 있습니다. Closeable은 IO와 관련된 AutoCloseable로 Exception이 아닌 IOException을 던질 것을 지시하고 있습니다.

FileInputStream의 close는 다음과 같이 오버라이딩 되어 있습니다.

try-with-resources를 활용하는 간단한 Use코드를 만들어 보겠습니다.
public class Use {
public void main() throws IOException {
try(InputStream is = new FileInputStream("file")) {
System.out.println("Hello World");
}
}
}
decompiled된 Use.class파일을 살펴보면 다음과 같습니다.
public class Use {
public Use() {
}
public void main() throws IOException {
InputStream is = new FileInputStream("file");
try {
System.out.println("Hello World");
} catch (Throwable var5) {
try {
is.close();
} catch (Throwable var4) {
var5.addSuppressed(var4);
}
throw var5;
}
is.close();
}
}
is.close()로 close메서드를 호출해 자원을 반납하고 있습니다.어차피 컴파일 시점에 직접 close()해주고 있기 때문에 try-with-resources는 아래의 try-finally와 크게 다를게 없는 걸까요?
public class Use {
public void main() throws IOException {
InputStream is = new FileInputStream("file");
try {
System.out.println("Hello World");
} finally {
is.close();
}
}
}
public class CloseableClass implements Closeable {
private String name;
public CloseableClass(String name) {
this.name = name;
}
@Override
public void close() throws IOException {
System.out.printf("close method in %s class", name);
}
}
public class CloseableButFailClass implements Closeable {
private String name;
public CloseableButFailClass(String name) {
this.name = name;
}
@Override
public void close() throws IOException {
System.out.printf("close method in %s class", name);
throw new IOException("close fail");
}
}
@Test
void closeLeakTest() throws IOException {
CloseableClass closeableClass = new CloseableClass("A");
CloseableButFailClass closeableButFailClass = new CloseableButFailClass("B");
try {
System.out.println("do works");
} finally {
closeableButFailClass.close();
closeableClass.close();
}
}
closeableClass.close()를 실행했기 때문에 무슨 일이 있어도 closeableClass의 자원이 반납될 것을 기대하고 있습니다. (왜냐하면 'finally에 선언한 코드는 반드시 실행된다'라고 생각하는 경우가 많기 때문입니다)
우리는 보통 try블록에서 메인 로직을 실행하고, 거기서 발생한 예외를 통해 문제를 파악합니다.
@Test
void errorTraceTest() {
try {
throw new RuntimeException("main error");
}catch (Exception e){
throw e;
}
}

만약 finally블록에서 예외가 발생하면 다음과 같이 finally의 예외만 출력됩니다.
@Test
void errorTraceTest() {
try {
throw new RuntimeException("main error");
}catch (Exception e){
throw e;
} finally {
// finally에서 로직(ex-자원 반납)을 진행하다가 에러가 발생
throw new RuntimeException("finally error");
}
}

자원을 반납하다 발생한 예외도 중요합니다. 하지만 보다 근본적으로 우리는 try블록의 메인 로직에서 발생한 예외가 무엇인지를 알아야 합니다.
하지만 finally에서 예외가 발생하면 메인 예외는 출력되지 않아 이를 확인할 수 없습니다.
try-with-resources는 이러한 문제를 해결해 줍니다.
public class MyClass implements Closeable {
public void mainLogic() {
throw new RuntimeException("main error");
}
@Override
public void close() {
throw new RuntimeException("close error");
}
}
public class Use {
public static void main() {
try(MyClass myClass = new MyClass()) {
myClass.mainLogic();
}
}
}
@Test
void errorTraceTest() {
Use.main();
}

try-with-resources를 사용한 Use클래스의 decompiled된 Use.class파일을 살펴봅시다.
public class Use {
public Use() {
}
public static void main() {
MyClass cc = new MyClass();
try {
cc.mainLogic();
} catch (Throwable var4) {
try {
cc.close();
} catch (Throwable var3) {
var4.addSuppressed(var3);
}
throw var4;
}
cc.close();
}
}
try-with-resources대신 try-finally에서 addSuppressed를 적절히 활용하면 그때는 똑같이 예외를 추적해 주는거 아니냐? 라고 물으면 틀린 말은 아닐 겁니다. 하지만 어차피 똑같다면 이런 번거로운 코드를 직접 설계하기 보다 간편한 try-with-resources를 사용하는게 더 합리적인 선택일 겁니다.