F-LAB JAVA · 3주차 · Phase 9 · I/O 강화
🚀 Phase 9 시작 — Stream 의 강화
이 Unit을 끝내면 다음을 답할 수 있어야 한다.
try-with-resources 는 "자원 자동 close" 를 보장하는 Java 7+ 의 구조다.
기존 try-finally 의 4가지 문제 — 길고 복잡, 예외 마스킹, close 누락 위험, 가독성 — 를 해결.
컴파일러가 자동으로 finally + close 코드를 생성하며,
Suppressed Exception 으로 close 시 예외가 원래 예외를 가리는 문제도 해결.
AutoCloseable 인터페이스 구현 객체만 사용 가능, Java 9+ 부터 effectively final 자원도 지원.
try-finally (수동 반납):
도서관에서 책 빌리기
- 손으로 책 빌림
- 다 보면 손으로 직접 반납해야
- 깜빡하면? 연체료
- 여러 권이면? 모두 반납 신경 써야
try-with-resources (자동 반납):
- 시간 지나면 자동 반납
- 깜빡함 없음
- 여러 권도 자동
- 책 사용에만 집중 가능
→ try-with-resources = 자원 자동 관리.
1. 자원의 정의와 수동 관리의 문제
2. try-finally 의 4가지 구조적 문제
3. try-with-resources 의 동작
4. AutoCloseable 인터페이스
5. 컴파일러의 디슈가링
6. Suppressed Exception
7. 자원의 close 순서
8. Java 9+ 의 개선과 실무 패턴
9. 면접 + 자기 점검
자원 (Resource):
사용 후 명시적으로 해제해야 하는 객체.
종류:
- 파일 핸들 (FileInputStream, FileOutputStream)
- 네트워크 연결 (Socket, HttpURLConnection)
- DB 연결 (Connection, Statement, ResultSet)
- 스트림 (BufferedReader, BufferedWriter)
- Lock (ReentrantLock 등)
- 시스템 자원 (메모리 매핑된 버퍼)
자원을 안 닫으면:
1. 파일 핸들 누수
- OS 가 파일 핸들 한정 보유
- 새 파일 못 열음 → "Too many open files"
2. DB 연결 누수
- Connection Pool 고갈
- 모든 요청 timeout
3. 메모리 누수
- GC 가 정리 못 함 (외부 자원)
- 메모리 점진 증가
4. 락 누수
- 다른 스레드 영원히 대기
- 데드락
// 자원 해제 안 한 잘못된 코드
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
String line = reader.readLine();
reader.close(); // ★ 정상 흐름에서만 닫힘
return line;
}
// 문제: readLine() 에서 IOException 발생하면?
// → reader.close() 실행 안 됨
// → 자원 누수!
// 안전한 패턴
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
return reader.readLine();
} finally {
reader.close(); // 항상 실행
}
}
// 보장:
// - 정상 흐름: 반환 후 close
// - 예외 흐름: 예외 전파 + close
// 두 자원 처리
public void copyFile(String src, String dest) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dest);
try {
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
// 중첩 try-finally
// 자원 늘어날수록 코드 폭증
// 가독성 ↓↓
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
return reader.readLine(); // ★ 여기서 예외 A 발생
} finally {
reader.close(); // ★ 여기서 예외 B 발생
// 예외 B 가 예외 A 를 가림
// 사용자는 close 의 예외만 보게 됨
// 진짜 원인 (예외 A) 가 숨겨짐
}
}
1. 코드 장황함
- 자원 1개에 try-finally 한 쌍
- 여러 자원이면 중첩 폭증
2. 가독성 ↓
- 본 로직과 자원 관리가 섞임
- 핵심 코드가 가려짐
3. 누락 위험
- close 호출 깜빡함
- 자원 누수
4. 예외 마스킹
- close 의 예외가 진짜 예외 가림
- 디버깅 어려움
자원의 수동 관리가 어려운 이유 4가지는?
답:
1. 코드 장황함: try-finally 중첩으로 복잡
2. 가독성 ↓: 본 로직과 자원 관리 혼재
3. 누락 위험: close 깜빡함 → 자원 누수
4. 예외 마스킹: close 의 예외가 진짜 예외 가림
→ try-with-resources 가 모두 해결.
// 자원 1개
public String firstLine(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
return reader.readLine();
} finally {
reader.close();
}
}
// 본 코드: 1줄 (return reader.readLine())
// 자원 관리: 3줄 (try { ... } finally { ... })
// 비율: 자원 관리 75%, 본 코드 25%
// 자원 3개
public void process(String src, String dest, String log) throws IOException {
InputStream in = new FileInputStream(src);
try {
OutputStream out = new FileOutputStream(dest);
try {
Writer logger = new FileWriter(log);
try {
// 본 코드
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
logger.write("done");
} finally {
logger.close();
}
} finally {
out.close();
}
} finally {
in.close();
}
}
// 자원 1개에 try-finally 한 쌍
// 자원 3개 = 3중 중첩
// 본 코드는 7줄, 자원 관리는 12줄
public void process() throws IOException {
Resource r = openResource();
try {
// 사용
} finally {
r.close(); // ★ close 가 IOException
// try 블록 결과를 못 봄
}
}
// 일부 자원의 close 도 throw 가능
// - Socket.close: IOException
// - Connection.close: SQLException
// - FileInputStream.close: IOException
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
throw new RuntimeException("진짜 원인"); // 예외 A
} finally {
reader.close(); // 예외 B (IOException)
// 예외 B 가 예외 A 를 마스크
}
}
// 호출자가 받는 예외:
// IOException (close 의 예외)
// RuntimeException 은 숨겨짐!
// 디버깅 시:
// - "왜 IOException?"
// - 진짜 원인을 찾기 어려움
public class Resource {
public void process() throws IOException {
throw new IOException("process failed (원인 A)");
}
public void close() throws IOException {
throw new IOException("close failed (마스킹 B)");
}
}
public static void test() {
Resource r = new Resource();
try {
r.process(); // A 발생
} catch (IOException e) {
// 도달 못 함
} finally {
try {
r.close(); // B 발생, A 를 가림
} catch (IOException e) {
// close 의 예외만 처리
}
}
}
// 호출자는 "close failed" 만 보고
// "process failed" 의 진짜 원인을 못 봄
// ❌ 잘못 — close 호출 안 함
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
return reader.readLine(); // close 호출 X
// 예외 발생 시 자원 누수
}
// ❌ 잘못 — close 위치
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
String line = reader.readLine();
reader.close();
return line;
// 예외 발생 시 close 안 됨
}
// ✓ 올바름
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
return reader.readLine();
} finally {
reader.close();
}
}
1. 코드 길이
- 한 자원에 3-4줄 추가
- 여러 자원이면 더
2. 중첩 복잡
- 다중 자원 → 다중 중첩
- 가독성 ↓
3. close 자체 예외
- 별도 try-catch 필요
4. 예외 마스킹
- 진짜 원인 숨겨짐
- 디버깅 어려움
5. 인지 부담
- 매번 패턴 작성
- 깜빡함 위험
→ try-with-resources (Java 7+) 로 해결
try-finally 의 4가지 구조적 문제는?
답:
1. 코드 장황함: 1자원에 3-4줄 추가
2. 중첩 복잡: 다중 자원 시 코드 폭증
3. close 자체 예외: 별도 try-catch 필요
4. 예외 마스킹: close 의 예외가 진짜 원인 가림
→ try-with-resources 가 모두 해결.
// 단일 자원
public String firstLine(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
}
}
핵심:
try ( ... ) 안에 자원 선언// 여러 자원 — 세미콜론으로 구분
public void copyFile(String src, String dest) throws IOException {
try (
InputStream in = new FileInputStream(src);
OutputStream out = new FileOutputStream(dest);
Writer log = new FileWriter("copy.log")
) {
byte[] buf = new byte[1024];
int n;
while ((n = in.read(buf)) >= 0) {
out.write(buf, 0, n);
}
log.write("done");
}
// log, out, in 순서로 자동 close
}
public String readWithCatch(String path) {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
} catch (IOException e) {
return "error: " + e.getMessage();
}
// close 는 자동 호출 후 catch 실행
}
public String readWithFinally(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
} finally {
System.out.println("정리 작업");
// close 후에 finally 실행
}
}
// try-finally (Java 6)
public String readFile(String path) throws IOException {
BufferedReader reader = new BufferedReader(new FileReader(path));
try {
return reader.readLine();
} finally {
reader.close();
}
}
// try-with-resources (Java 7+)
public String readFile(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
}
}
// 차이:
// 1. 코드 줄 수 ↓
// 2. close 자동
// 3. 예외 마스킹 해결 (다음 섹션)
// 4. 가독성 ↑
public class ShipmentExporter {
// 단일 자원
public List<Shipment> readShipments(String path) throws IOException {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.lines()
.map(this::parseShipment)
.toList();
}
}
// 다중 자원
public void exportShipments(List<Shipment> shipments, String outPath, String logPath)
throws IOException {
try (
Writer out = new FileWriter(outPath);
Writer log = new FileWriter(logPath);
PrintWriter pw = new PrintWriter(out)
) {
for (Shipment s : shipments) {
pw.println(s.toCsvLine());
log.write("Exported: " + s.getBlNo() + "\n");
}
}
// pw, log, out 순서로 자동 close
}
// DB 작업
public List<Shipment> findAllFromDb() throws SQLException {
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM shipments");
ResultSet rs = ps.executeQuery()
) {
List<Shipment> result = new ArrayList<>();
while (rs.next()) {
result.add(mapShipment(rs));
}
return result;
}
// rs, ps, conn 순서로 close
}
}
public String readFile(String path) {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
} catch (IOException e) {
// 1. try 블록 또는 자원 생성에서 IOException 발생
// 2. close 후 catch 실행
// 3. close 의 예외는 suppressed 로
return "error: " + e.getMessage();
}
}
try-with-resources 의 기본 사용은?
답:
1. 단일 자원:
try (Resource r = new Resource()) {
// 사용
}
다중 자원:
try (
Resource1 r1 = ...;
Resource2 r2 = ...
) {
// 사용
}
catch/finally 결합:
장점:
package java.lang;
public interface AutoCloseable {
void close() throws Exception;
}
특징:
java.lang 패키지close()throws Exception (모든 예외)// 더 좁은 인터페이스
package java.io;
public interface Closeable extends AutoCloseable {
@Override
void close() throws IOException; // ★ IOException 만
}
차이:
계층:
AutoCloseable (Java 7+, 모든 자원)
└── Closeable (Java 5+, I/O 자원)
// AutoCloseable 구현하는 모든 클래스 사용 가능
try (AutoCloseable r = ...) { }
// Closeable 도 자동 지원 (AutoCloseable 의 자식)
try (Closeable c = ...) { }
// 자바 표준의 주요 AutoCloseable 구현
- InputStream, OutputStream (Closeable)
- Reader, Writer (Closeable)
- Socket (Closeable)
- Connection, Statement, ResultSet (AutoCloseable)
- Stream<T> (AutoCloseable)
- Lock (사용자 정의)
- HttpURLConnection (AutoCloseable since Java 9)
public class ShipmentLock implements AutoCloseable {
private final ReentrantLock lock;
public ShipmentLock() {
this.lock = new ReentrantLock();
lock.lock();
System.out.println("Lock acquired");
}
@Override
public void close() {
lock.unlock();
System.out.println("Lock released");
}
}
// 사용
try (ShipmentLock l = new ShipmentLock()) {
// 임계 구역
processShipment();
}
// 자동으로 unlock
// AutoCloseable
void close() throws Exception;
// Closeable
void close() throws IOException;
// 둘 다 가능
public class MyResource implements AutoCloseable {
@Override
public void close() throws IOException { // 더 좁은 예외
// ...
}
}
public class StrictResource implements AutoCloseable {
@Override
public void close() { // 예외 없음 (가장 좁음)
// ...
}
}
AutoCloseable.close() 의 javadoc:
"재진입 가능 (Idempotent) 하게 구현 권장"
- 여러 번 호출해도 안전
- 두 번째부터는 no-op
"예외 발생 시 자원이 깨끗하게 정리되었는지 명확히"
"InterruptedException 던지지 말기"
- Thread 상태 영향
public class ManagedConnection implements AutoCloseable {
private final Connection delegate;
private final ConnectionPool pool;
public ManagedConnection(Connection conn, ConnectionPool pool) {
this.delegate = conn;
this.pool = pool;
}
public PreparedStatement prepareStatement(String sql) throws SQLException {
return delegate.prepareStatement(sql);
}
@Override
public void close() {
// 실제로는 풀에 반환
pool.release(delegate);
}
}
// 사용
try (ManagedConnection conn = pool.acquire()) {
PreparedStatement ps = conn.prepareStatement("SELECT * FROM ...");
// ...
}
// 자동으로 pool 에 반환
// 모든 I/O 자원
try (InputStream in = new FileInputStream("file.txt")) { ... }
try (BufferedReader reader = new BufferedReader(...)) { ... }
try (PrintWriter writer = new PrintWriter("out.txt")) { ... }
// 네트워크
try (Socket s = new Socket(host, port)) { ... }
// DB
try (
Connection conn = ...;
PreparedStatement ps = ...;
ResultSet rs = ...
) { ... }
// Stream
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.forEach(System.out::println);
}
// 락
try (ShipmentLock lock = new ShipmentLock()) { ... }
AutoCloseable 과 Closeable 의 차이는?
답:
AutoCloseable (java.lang, Java 7+):
close() throws ExceptionCloseable (java.io, Java 5+):
close() throws IOException선택:
디슈가링 (Desugaring):
고수준 문법 → 저수준 문법 변환.
컴파일러가 try-with-resources 를
try-finally 코드로 변환.
// 원본 (try-with-resources)
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
}
// 디슈가링 (try-finally 로 변환)
BufferedReader reader = new BufferedReader(new FileReader(path));
Throwable primaryException = null;
try {
return reader.readLine();
} catch (Throwable t) {
primaryException = t;
throw t;
} finally {
if (reader != null) {
if (primaryException != null) {
try {
reader.close();
} catch (Throwable suppressed) {
primaryException.addSuppressed(suppressed); // ★
}
} else {
reader.close();
}
}
}
핵심:
// 원본
try (
Resource1 r1 = new Resource1();
Resource2 r2 = new Resource2()
) {
// 사용
}
// 디슈가링 (개념적)
Resource1 r1 = new Resource1();
try {
Resource2 r2 = new Resource2();
try {
// 사용
} finally {
r2.close();
}
} finally {
r1.close();
}
// 또는 더 정확히:
Resource1 r1 = new Resource1();
Throwable t1 = null;
try {
Resource2 r2 = new Resource2();
Throwable t2 = null;
try {
// 사용
} catch (Throwable t) {
t2 = t;
throw t;
} finally {
if (r2 != null) {
if (t2 != null) {
try { r2.close(); } catch (Throwable s) { t2.addSuppressed(s); }
} else {
r2.close();
}
}
}
} catch (Throwable t) {
t1 = t;
throw t;
} finally {
if (r1 != null) {
if (t1 != null) {
try { r1.close(); } catch (Throwable s) { t1.addSuppressed(s); }
} else {
r1.close();
}
}
}
→ 자원 N 개 → 중첩 N 단계 finally.
디슈가링의 흥미로운 면:
자원 선언 순서: r1, r2, r3
close 순서: r3, r2, r1 (역순!)
이유:
- 자원 의존성 (r3 가 r2 의존, r2 가 r1 의존)
- 마지막에 만들어진 게 먼저 닫혀야
비유:
레고 조립 — 마지막 블록 먼저 분해
// 원본
public class Demo {
public void test() throws IOException {
try (FileInputStream in = new FileInputStream("test.txt")) {
in.read();
}
}
}
// javac 컴파일 후 javap -c 로 보면:
// 실제로는 더 복잡한 바이트코드
// 하지만 개념적으로는 위의 try-finally 변환
Java 7:
- 명시적 finally + 변수 추적
- 비교적 복잡한 바이트코드
Java 8+:
- 더 효율적인 바이트코드
- JIT 최적화 잘됨
Java 11+:
- 추가 최적화
try-with-resources 가 보장하는 것들:
1. close 자동 호출
- try 블록 정상 종료 시
- 예외 발생 시
2. 자원 변수가 null 인 경우 안전
- if (resource != null) 체크
3. close 의 예외 처리
- 정상 흐름: throw
- 예외 흐름: addSuppressed
4. 다중 자원의 역순 close
- 마지막 → 처음
5. catch 블록과의 정확한 순서
- close → catch → finally
컴파일러가 try-with-resources 를 어떻게 풀어내나?
답:
1. 자원 변수 선언: try 외부로 이동
2. try-finally 생성: 자동
3. close 호출: finally 안에
4. Suppressed Exception: 예외 충돌 시 addSuppressed
5. 다중 자원: 중첩 finally (역순 close)
// 컴파일러 출력 (개념적):
Resource r = ...;
Throwable primary = null;
try {
// 사용
} catch (Throwable t) {
primary = t;
throw t;
} finally {
if (r != null) {
if (primary != null) {
try { r.close(); }
catch (Throwable s) { primary.addSuppressed(s); }
} else {
r.close();
}
}
}
Suppressed Exception:
주 예외 (primary) 가 발생한 후 추가로 발생한 예외.
주 예외에 부가 정보로 첨부.
용도:
- close 가 던진 예외가 try 의 예외를 가리는 문제 해결
- 모든 예외 정보 보존
public class Throwable {
private List<Throwable> suppressedExceptions;
public final void addSuppressed(Throwable exception) {
// 자기 자신에 부가 예외 추가
}
public final Throwable[] getSuppressed() {
return suppressedExceptions.toArray(...);
}
}
특징:
// try-finally — 예외 마스킹
public void test() {
Resource r = new Resource();
try {
throw new RuntimeException("A"); // 원인
} finally {
r.close(); // throw new IOException("B");
}
// 호출자가 받는 예외:
// → IOException "B" (A 가 가려짐)
}
// try-with-resources — Suppressed 활용
public void test() throws Exception {
try (Resource r = new Resource()) {
throw new RuntimeException("A");
}
// 호출자가 받는 예외:
// → RuntimeException "A"
// → A.getSuppressed() = [IOException "B"]
// 둘 다 보존!
}
public class SuppressedDemo {
static class FailingResource implements AutoCloseable {
@Override
public void close() throws IOException {
throw new IOException("close failed");
}
}
public static void main(String[] args) {
try {
try (FailingResource r = new FailingResource()) {
throw new RuntimeException("primary failure");
}
} catch (Exception e) {
System.out.println("Primary: " + e.getMessage());
// → Primary: primary failure
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("Suppressed: " + suppressed.getMessage());
}
// → Suppressed: close failed
}
}
}
// 예외 출력 (e.printStackTrace())
java.lang.RuntimeException: primary failure
at SuppressedDemo.main(SuppressedDemo.java:13)
Suppressed: java.io.IOException: close failed
at SuppressedDemo$FailingResource.close(SuppressedDemo.java:7)
at SuppressedDemo.main(SuppressedDemo.java:12)
// "Suppressed:" 표시
// 두 예외 모두 보임
// 디버깅 명확
public String readWithRetry(String path) {
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
} catch (IOException e) {
// 메인 예외 처리
log.error("Read failed", e);
// Suppressed 도 확인
for (Throwable suppressed : e.getSuppressed()) {
log.warn("Suppressed during close", suppressed);
}
return null;
}
}
try (
Resource r1 = new Resource("r1"); // close 도 예외
Resource r2 = new Resource("r2"); // close 도 예외
) {
throw new RuntimeException("primary");
}
// 결과:
// - primary: RuntimeException
// - suppressed: [r2.close 예외, r1.close 예외]
// → 역순 close + 둘 다 suppressed
// 직접 사용 (드물게)
RuntimeException primary = new RuntimeException("primary");
try {
cleanup();
} catch (Exception cleanupEx) {
primary.addSuppressed(cleanupEx);
}
throw primary;
// try-with-resources 는 자동
Suppressed Exception 의 역할은?
답:
1. 예외 마스킹 해결:
try-with-resources 의 자동 처리:
활용:
getSuppressed() 로 접근다중 자원:
// 자원 선언 순서: r1, r2, r3
try (
Resource1 r1 = new Resource1();
Resource2 r2 = new Resource2();
Resource3 r3 = new Resource3()
) {
// 사용
}
// close 순서:
// 1. r3.close() ← 마지막에 만든 것 먼저
// 2. r2.close()
// 3. r1.close()
이유:
1. 의존성
- r2 가 r1 의 자원 의존 가능
- r1 먼저 닫으면 r2 의 close 실패 가능
- r2 → r1 순서가 안전
2. LIFO (Last In, First Out)
- 스택과 같은 원리
- 마지막 생성 = 가장 의존성 적음
- 먼저 닫혀도 안전
3. 자바 표준
- DB: ResultSet → Statement → Connection
- I/O: 외부 스트림 → 내부 스트림
- 의존성 역방향
try (
Connection conn = dataSource.getConnection(); // 1. 먼저
PreparedStatement ps = conn.prepareStatement(sql); // 2. Connection 의존
ResultSet rs = ps.executeQuery() // 3. Statement 의존
) {
// 사용
}
// close 순서:
// 1. rs.close() ← ResultSet 먼저
// 2. ps.close() ← Statement
// 3. conn.close() ← Connection 마지막
// 이유:
// - rs 가 ps 의존
// - ps 가 conn 의존
// - 역순 close 가 안전
try (
FileInputStream fis = new FileInputStream("file.txt");
BufferedInputStream bis = new BufferedInputStream(fis);
DataInputStream dis = new DataInputStream(bis)
) {
int value = dis.readInt();
}
// close 순서:
// 1. dis.close() ← Decorator pattern
// 2. bis.close()
// 3. fis.close()
// ❌ 잘못된 선언 순서
try (
PreparedStatement ps = conn.prepareStatement(sql); // ❌ conn 먼저 필요
Connection conn = dataSource.getConnection()
) {
// 컴파일 에러
// ps 선언 시 conn 이 아직 없음
}
// ✓ 올바른 순서
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)
) {
// ...
}
// 모든 자원이 close 실행됨 (한 자원이 실패해도)
try (
Resource r1 = new Resource();
Resource r2 = new Resource();
Resource r3 = new Resource()
) {
// ...
}
// 가정: r2.close 가 예외
// 흐름:
// 1. r3.close() — 정상
// 2. r2.close() — 예외 (suppressed 또는 primary)
// 3. r1.close() — 여전히 실행됨
// 핵심: 한 자원의 실패가 다른 자원의 close 막지 않음
// 사용자 자원의 의존
public class WrappedResource implements AutoCloseable {
private final InputStream inner;
public WrappedResource(InputStream inner) {
this.inner = inner;
}
@Override
public void close() throws IOException {
try {
// wrapper 정리
} finally {
inner.close(); // 내부 자원도 닫기
}
}
}
// 사용
try (WrappedResource w = new WrappedResource(fis)) {
// ...
}
// w.close() → fis.close()
다중 자원의 close 순서와 그 이유는?
답:
1. 역순 (LIFO):
이유:
예:
한 자원의 실패가 다른 자원에 영향 X:
// Java 7, 8 — 자원을 try 안에 선언해야
public void process(BufferedReader reader) throws IOException {
// try (reader) { ... } // ❌ Java 8 까지 불가
try (BufferedReader r = reader) { // 새 변수 필요
// ...
}
}
// Java 9+ — effectively final 변수 직접 사용
public void process(BufferedReader reader) throws IOException {
try (reader) { // ✓ Java 9+
// ...
}
}
effectively final:
- 한 번 할당되고 다시 할당 안 됨
- final 키워드 없어도 OK
- 람다와 try-with-resources 가 활용
조건:
- 변수 선언 후 한 번만 할당
- 그 외 변경 없음
// 메서드 매개변수
public void process(InputStream in) throws IOException {
try (in) { // ✓ Java 9+
byte[] buf = in.readAllBytes();
// ...
}
}
// 필드
private InputStream resource;
public void process() throws IOException {
try (resource) { // ✓
// ...
}
}
// 로컬 변수 (사전 선언)
InputStream in = openStream();
try (in) { // ✓ effectively final 이면
// ...
}
// in 을 try 후에 재할당 안 함
// Stream API — AutoCloseable
try (Stream<String> lines = Files.lines(Paths.get("file.txt"))) {
lines.forEach(System.out::println);
}
// JDBC
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()
) {
while (rs.next()) {
// ...
}
}
// HttpClient (Java 11+)
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder(URI.create(url)).build();
try {
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
// HttpClient 도 AutoCloseable (Java 21+)
} catch (...) { ... }
public class FileProcessor {
// 읽기
public List<String> readLines(String path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(Paths.get(path))) {
return reader.lines().toList();
}
}
// 쓰기
public void writeLines(String path, List<String> lines) throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(path))) {
for (String line : lines) {
writer.write(line);
writer.newLine();
}
}
}
// Stream 활용
public long countWords(String path) throws IOException {
try (Stream<String> lines = Files.lines(Paths.get(path))) {
return lines.flatMap(line -> Arrays.stream(line.split("\\s+")))
.count();
}
}
}
public class ShipmentRepository {
private final DataSource dataSource;
public List<Shipment> findAll() throws SQLException {
String sql = "SELECT id, bl_no, weight, created_at FROM shipments";
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()
) {
List<Shipment> result = new ArrayList<>();
while (rs.next()) {
result.add(mapShipment(rs));
}
return result;
}
}
public void save(Shipment shipment) throws SQLException {
String sql = "INSERT INTO shipments (bl_no, weight) VALUES (?, ?)";
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)
) {
ps.setString(1, shipment.getBlNo());
ps.setBigDecimal(2, shipment.getWeight());
ps.executeUpdate();
}
}
}
public class HttpClientWrapper {
public String fetchUrl(String urlStr) throws IOException {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream()))) {
return reader.lines().collect(Collectors.joining("\n"));
}
}
public byte[] downloadFile(String urlStr) throws IOException {
URL url = new URL(urlStr);
try (InputStream in = url.openStream()) {
return in.readAllBytes();
}
}
}
public class ShipmentLockManager {
private final Map<Long, ReentrantLock> locks = new ConcurrentHashMap<>();
public AutoCloseable acquireLock(Long shipmentId) {
ReentrantLock lock = locks.computeIfAbsent(shipmentId, k -> new ReentrantLock());
lock.lock();
return lock::unlock; // ★ AutoCloseable 람다? 직접 안 됨
// 다음 패턴
}
}
// 더 좋은 패턴
public class ScopedLock implements AutoCloseable {
private final ReentrantLock lock;
public ScopedLock(ReentrantLock lock) {
this.lock = lock;
lock.lock();
}
@Override
public void close() {
lock.unlock();
}
}
// 사용
try (ScopedLock l = new ScopedLock(myLock)) {
// 임계 구역
}
@Service
public class ShipmentService {
private final DataSource dataSource;
private final Path exportDir;
// DB + 파일 결합
public void exportShipments() throws IOException, SQLException {
Path outPath = exportDir.resolve("shipments_" + LocalDate.now() + ".csv");
try (
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement("SELECT * FROM shipments");
ResultSet rs = ps.executeQuery();
BufferedWriter writer = Files.newBufferedWriter(outPath);
PrintWriter pw = new PrintWriter(writer)
) {
// 헤더
pw.println("id,bl_no,weight,created_at");
// 데이터
while (rs.next()) {
pw.printf("%d,%s,%s,%s%n",
rs.getLong("id"),
rs.getString("bl_no"),
rs.getBigDecimal("weight"),
rs.getTimestamp("created_at"));
}
}
// 모든 자원 자동 정리
}
}
Java 9+ 의 effectively final 자원의 효과는?
답:
이전 (Java 7, 8):
public void process(BufferedReader reader) {
try (BufferedReader r = reader) { ... } // 새 변수 필요
}
Java 9+:
public void process(BufferedReader reader) {
try (reader) { ... } // 직접 사용
}
효과:
조건:
| Q | 핵심 답변 |
|---|---|
| try-with-resources 등장 이유? | try-finally 의 4가지 문제 해결 |
| try-finally 의 문제 4가지? | 길이, 중첩, close 예외, 마스킹 |
| try-with-resources 동작? | 자동 close, Suppressed 활용 |
| AutoCloseable 정의? | java.lang, 단일 close() |
| Closeable 과 차이? | I/O 전용 IOException |
| Suppressed Exception? | 마스킹 해결, 모든 예외 보존 |
| close 순서? | 역순 (LIFO) |
| 역순의 이유? | 의존성 + 안전 |
| Java 9 개선? | effectively final 자원 |
| try-with-resources 와 catch? | close → catch → finally |
| 컴파일러가 어떻게? | 자동 try-finally + Suppressed |
답:
public class IdempotentResource implements AutoCloseable {
private boolean closed = false;
@Override
public void close() {
if (closed) return; // 두 번째 호출 무시
closed = true;
// 자원 해제
}
}
답:
try (Resource r = ...) {
r = newResource(); // ❌ 컴파일 에러 (final)
}
답:
try (Resource r = mayBeNull()) { // r 이 null 일 수도
// 사용
}
// close 시: if (r != null) r.close();
// 컴파일러가 null 체크 자동 추가
답:
try (
Resource1 r1 = new Resource1(); // 성공
Resource2 r2 = new Resource2() // ❌ 예외
) {
// 도달 못 함
}
// r1.close() 호출됨
// 예외는 호출자로 전파
답:
public String test() {
try (Resource r = new Resource()) {
return "value";
}
// 흐름:
// 1. "value" 평가
// 2. r.close()
// 3. "value" 반환
}
1. try-with-resources 등장
2. 동작 메커니즘
3. 실무 활용
이번 Unit에서 자원 관리 (try-with-resources) 를 봤다면, 다음은 버퍼링 스트림의 정밀.
🚀 Phase 9 — I/O 강화
✅ Unit 9.1 try-with-resources ← 여기
⏭ Unit 9.2 BufferedInputStream / BufferedOutputStream
⏭ Unit 9.3 DataInputStream / DataOutputStream
⏭ Unit 9.4 Serialization (직렬화)
⏭ Unit 9.5 serialVersionUID
✅ Phase 1 — Pass by Value (1.1 ~ 1.3 완주)
✅ Phase 2 — 컬렉션 프레임워크 (2.1 ~ 2.6 완주)
✅ Phase 3 — 해시의 원리 (3.1 ~ 3.4 완주)
✅ Phase 4 — 추상화의 두 도구 (4.1 ~ 4.4 완주)
✅ Phase 5 — 제네릭과 와일드카드 (5.1 ~ 5.5 완주)
✅ Phase 6 — 객체 비교 (6.1 ~ 6.4 완주)
✅ Phase 7 — I/O 시스템 큰 그림 (7.1 ~ 7.5 완주)
✅ Phase 8 — Stream 실전 (8.1 ~ 8.6 완주)
🚀 Phase 9 — I/O 강화 (1/5 진행)
총: 38/43 Unit 작성 (약 88%)
🚀 Phase 9 시작 — Stream 의 강화 진입