try-with-resources와 오류 보존
파일과 소켓처럼 사용 뒤 닫아야 하는 자원은 정상 경로뿐 아니라 모든 실패 경로에서 정리돼야 합니다.
try-with-resources는 AutoCloseable 자원을 선언 순서의 역순으로 자동 종료합니다.
본문 예외와 close 예외가 함께 나면 본문 예외를 주 예외로 유지하고 닫기 예외를 suppressed 목록에 보존합니다.
직접 finally에서 닫는 코드는 이 정보를 덮어쓰기 쉽습니다.
이 절에서는 자원 소유권과 실패 우선순위를 확인합니다.
자원 선언부 자체에서 실패할 수도 있습니다. 두 번째 자원을 만들다 예외가 나면 이미 만들어진 첫 번째 자원은 자동으로 닫힙니다. try-with-resources는 본문뿐 아니라 “일부만 획득된 상태”까지 수명 규칙 안에 넣습니다.
AutoCloseable 자원을 괄호 안에 선언한다
try 블록을 벗어날 때 성공 여부와 관계없이 close가 호출됩니다.
Java 9 이후 effectively final 지역 자원도 괄호에서 참조할 수 있습니다.
여러 자원은 선언의 역순으로 닫힌다
Reader가 InputStream 위에 있다면 Reader를 나중에 선언해 먼저 닫히게 합니다.
의존하는 바깥 래퍼부터 정리됩니다.
예제는 본문 실패 하나와 닫기 실패 둘을 일부러 동시에 만들어, 어느 오류가 주 원인으로 남는지 보여 줍니다.
public class ResourceFailures {
static final class DemoResource implements AutoCloseable {
private final String name;
DemoResource(String name) { this.name = name; }
void use() { throw new IllegalStateException("use:" + name); }
@Override public void close() { throw new IllegalStateException("close:" + name); }
}
public static void main(String[] args) {
try (DemoResource first = new DemoResource("first");
DemoResource second = new DemoResource("second")) {
second.use();
} catch (Exception error) {
System.out.println("main=" + error.getMessage());
for (Throwable suppressed : error.getSuppressed()) {
System.out.println("suppressed=" + suppressed.getMessage());
}
}
}
}use:second가 작업을 중단시킨 주 예외입니다. 그 뒤 second, first 순서로 close가 호출되고 두 실패는 getSuppressed() 배열에 보존됩니다. 마지막 close 오류로 앞선 원인을 덮지 않습니다.
main=use:second
suppressed=close:second
suppressed=close:first본문 use 실패가 주 예외로 남고 두 자원의 close 실패는 선언 역순으로 suppressed에 보존됩니다.
try 블록을 벗어날 때 성공 여부와 관계없이 close가 호출됩니다.
Reader가 InputStream 위에 있다면 Reader를 나중에 선언해 먼저 닫히게 합니다.
close도 실패하면 getSuppressed()로 추가 예외를 확인할 수 있습니다.
본문 실패가 주 예외로 남는다
close도 실패하면 getSuppressed()로 추가 예외를 확인할 수 있습니다.
원래 작업 실패를 잃지 않습니다.
실제 파일 API와 비교하면 문법보다 소유권 결정이 먼저라는 점이 드러납니다.
java.nio.file.Path path = java.nio.file.Path.of("sample.txt");
try (java.io.BufferedReader reader = java.nio.file.Files.newBufferedReader(path)) {
System.out.println(reader.readLine());
}// BufferedReader가 메서드 밖에서 전달됐다면 소유권 계약 없이 닫지 않는다.
// 직접 finally에서 close 예외를 던지면 본문 예외를 가릴 수 있다.자원을 만든 계층이 닫을 책임을 갖는다
호출자가 전달한 스트림을 임의로 닫으면 이후 사용을 깨뜨릴 수 있습니다.
소유권을 API 이름과 문서로 명시합니다.
닫기 역순은 의존 관계를 보호한다
여러 자원은 선언의 역순으로 닫힙니다.
바깥 자원이 안쪽 자원보다 오래 살아야 하는 구조를 선언 순서에 반영합니다.
호출자가 소유한 자원은 임의로 닫지 않는다
메서드가 생성한 자원은 메서드가 닫고 외부에서 받은 스트림은 소유권 계약을 따릅니다.
소유자를 문서와 타입 경계에서 명확히 합니다.
DemoResource.use()가 정상 종료하도록 바꿔 보세요. 이제 본문 예외가 없으므로 먼저 발생한 close:second가 주 예외가 되고 close:first만 suppressed로 남아야 합니다. 주 예외 선택이 단순히 선언 순서가 아니라 실패가 발생한 단계에 달렸음을 확인할 수 있습니다.
스트림을 반환하는 메서드는 누가 닫을지 특히 분명히 해야 합니다. 메서드 내부에서 모두 소비해 값으로 반환하면 내부가 소유하고, 열린 스트림 자체를 반환하면 일반적으로 호출자가 소유합니다. 이 계약이 불명확하면 조기 close와 자원 누수 양쪽이 생깁니다.
try-with-resources는 close 호출을 줄이는 문법이 아니라 자원 수명과 여러 실패의 보존 순서를 구조적으로 보장합니다.