
자바 라이브러리와 프레임워크에는, 직접 자원을 닫아야 하는 클래스들이 많이있다.
InputStream, OutputStream, Reader, Writer (IO 관련)java.sql.Connection, Statement, ResultSet (JDBC 관련)JPA(Hibernate 등)를 사용할 때도 DB 커넥션을 잘 닫지 않으면
자원 닫기를 놓치게 되면 예측할 수 없는 오류로 이어지기 때문에 빠트리면 안되는 중요한 작업이다.
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();
}
}
br.readLine() 중 예외가 발생해도, finally 블록이 실행되며 br.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[BUFFER_SIZE];
int n;
while ((n = in.read(buf)) >= 0)
out.write(buf, 0, n);
} finally {
out.close();
} finally {
in.close();
}
}
public static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine(); // 여기서 예외 발생 가능
} finally {
// finally는 *꼭* 실행은 됨. 하지만 close()가 또 실패할 수 있음
br.close(); // 여기서도 IOException 가능
}
}
br.readLine()에서 예외가 발생하게 된다면, 장치가 이미 이상하기 때문에 닫는 과정에서 또 다른 IO 예외 close()에서도 예외가 발생할 수가 있다.close()를 시도하는 경우 발생한 두 번째 예외가 첫 번째 예외를 덮어써버리기 때문에 디버깅이 어려워진다.package item9;
import java.io.IOException;
class Resource implements AutoCloseable {
private final String name;
Resource(String name) {
this.name = name;
}
@Override
public void close() throws IOException {
System.out.println(name + " closing...");
throw new IOException("close()에서 발생한 예외: " + name);
}
void doSomething() throws Exception {
// 첫 번째 예외
throw new Exception("doSomething() 예외: " + name);
}
}
public class SuppressedExceptionFinallyDemo {
public static void main(String[] args) {
try {
testFinally();
} catch (Exception e) {
System.out.println("메인에서 잡은 예외: " + e);
// getSuppressed()를 출력해봐도 suppressed 예외가 없을 것
// 왜냐하면 try-finally 구조에선 첫 번째 예외가 덮어써지기 때문
for (Throwable t : e.getSuppressed()) {
System.out.println("숨겨진(suppressed) 예외: " + t);
}
}
}
static void testFinally() throws Exception {
Resource2 r = null;
try {
r = new Resource2("MyResource");
r.doSomething(); // 첫 번째 예외 발생
} finally {
if (r != null) {
r.close(); // 두 번째 예외
}
}
}
}

실행 흐름
1. r.doSomething()가 첫 번째 예외(Exception("doSomething() 예외..."))를 던진다.
2. finally 블록이 실행되며 r.close()에서 두 번째 예외(IOException("close()에서 발생한 예외..."))가 발생한다.
3. 자바는 두 번째 예외를 던져버리며, 첫 번째 예외 정보가 없어지게 된다(덮어써짐).
try-with-resources자바 7부터는 try-with-resources가 도입되면서, 위 문제들이 깔끔하게 해결되었다.
AutoCloseable 인터페이스AutoCloseable을 구현해야한다.AutoCloseable는 close() 메서드 하나만 정의되어 있는 인터페이스다.AutoCloseable을 구현해두었다.서드파티 라이브러리란, 자바 표준 라이브러리(공식 JDK) 외부에서 제공되는 라이브러리이다. 예를 들어서
org.apache.*,org.springframework.*등을 말한다.
static String firstLineOfFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
}
}
try( ... ) 안에 AutoCloseable 구현체를 생성한다.close()가 호출되어 자원 해제가 이루어진다.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-resources는 첫 번째 예외를 보존하고 두 번째 예외를 suppressed 상태로 보관한다.Throwable.getSuppressed() 메서드로 프로그램에서 얻게 할 수도 있게 되었다.package item9;
import java.io.IOException;
class Resource2 implements AutoCloseable {
private final String name;
Resource2(String name) {
this.name = name;
}
@Override
public void close() throws IOException {
System.out.println(name + " closing...");
throw new IOException("close()에서 발생한 예외: " + name);
}
void doSomething() throws Exception {
throw new Exception("doSomething() 예외: " + name);
}
}
public class SuppressedExceptionDemo {
public static void main(String[] args) {
try {
testSuppressed();
} catch (Exception e) {
// 여기서 예외를 잡아서 확인
System.out.println("메인에서 잡은 예외: " + e);
// 숨겨진(suppressed) 예외들도 확인
for (Throwable t : e.getSuppressed()) {
System.out.println("숨겨진(suppressed) 예외: " + t);
}
}
}
static void testSuppressed() throws Exception {
try (Resource2 r = new Resource2("MyResource")) {
r.doSomething(); // 첫 번째 예외 발생
}
}
}

실행 흐름
1. r.doSomething()에서 첫 번째 예외가 발생한다(예: Exception: doSomething() 예외: MyResource).
2. 블록이 끝나면서 r.close() 호출 → 두 번째 예외(IOException: close()에서 발생한 예외: MyResource)가 발생한다.
3. try-with-resources는 첫 번째 예외를 주 예외로 삼고 두 번째 예외를 suppressed로 붙여서 던진다.
4. main에서 잡은 뒤 getSuppressed()를 보면 숨겨진 예외로 두 번째 예외 정보를 볼 수 있다.
catch 절 함께 사용 가능static String firstLineOfFile(String path, String defaultVal) {
try (BufferedReader br = new BufferedReader(
new FileReader(path))) {
return br.readLine();
} catch (IOException e) {
return defaultVal;
}
}
catch 블럭에서 처리가 가능하다. try 블록을 중첩하지 않아도 여러 예외 처리를 깔끔하게 할 수 있다.try-with-resources를 사용하자.