AutoCloseable을 구현한 자원을 선언하면 컴파일러가 close() 호출을 대신 넣어 준다.
try (var conn = ds.getConnection();
var ps = conn.prepareStatement(SQL);
var rs = ps.executeQuery()) {
while (rs.next()) { ... }
} // rs → ps → conn 순서로 자동 close
수동 finally 가 왜 위험한가
InputStream in = null;
OutputStream out = null;
try {
in = ...; out = ...;
copy(in, out);
} finally {
if (in != null) in.close(); // ← 여기서 예외가 나면
if (out != null) out.close(); // ← 이 줄이 실행되지 않는다 → 누수
}
올바르게 쓰려면 중첩 try 를 또 써야 한다 자원이 셋이면 코드가 손댈 수 없이 커진다
- 실무에서 가장 흔한 자원 누수 원인이 바로 이 형태였다
억제된 예외(suppressed)
body 에서 예외 A, close() 에서 예외 B 가 나면?
-
수동 finally — B 가 A 를 덮어써 원인이 사라진다 (최악)
-
try-with-resources — A 를 던지고 B 를 A.getSuppressed() 에 담는다
-
"진짜 원인" 이 보존된다. 스택트레이스에 Suppressed: 로 찍힌다
닫는 순서와 effectively final
선언의 역순으로 닫는다 — 나중에 연 것(의존하는 쪽)부터
- ResultSet → Statement → Connection
Java 9 부터는 밖에서 만든 변수도 쓸 수 있다
- 단, effectively final 이어야 한다
- try (existingResource) { ... }
실무 주의점
- Stream 도 AutoCloseable 이다. 파일 기반(Files.lines) 은 반드시 닫아야 한다
- 컬렉션 기반 스트림은 닫을 필요가 없어 습관이 안 든다 → 파일에서 FD 누수
- 커넥션 풀에서 close() 는 '반납' 이다. 안 닫으면 풀이 마르고 전체가 멈춘다
- close() 가 조용히 실패하는 구현은 피한다 — 삼켜진 실패는 추적이 불가능하다
컴파일러가 실제로 만드는 코드
// 우리가 쓴 것
try (var a = open()) { use(a); }
// javac 가 펼친 것 (개념적으로)
var a = open();
Throwable primary = null;
try {
use(a);
} catch (Throwable t) {
primary = t; throw t;
} finally {
if (a != null) {
if (primary != null) {
try { a.close(); }
catch (Throwable sup) { primary.addSuppressed(sup); } // 원인 보존
} else {
a.close();
}
}
}
손으로 매번 이렇게 쓰는 것은 현실적이지 않다 — 그래서 문법으로 올렸다.
close() 는 멱등해야 한다
같은 자원을 두 번 닫는 상황은 실제로 자주 생긴다
- 중첩 try 에서 안팎이 모두 닫는다
- 프레임워크와 애플리케이션이 각자 닫는다
AutoCloseable 규약은 멱등을 '권장' 하고, Closeable(java.io) 은 멱등을 '요구' 한다 직접 구현할 때는 닫힘 플래그를 두는 편이 안전하다
면접 함정
- ❌ "finally에서 닫으면 같다" → close 중 예외에서 원인이 소실되고 뒷 자원이 누수된다.
- ❌ "모든 Stream을 닫아야 한다" → 파일·소켓 기반만 필요하다.