3주차 Unit 9.1 — try-with-resources

Psj·2026년 5월 20일

F-lab

목록 보기
112/197

Unit 9.1 — try-with-resources

F-LAB JAVA · 3주차 · Phase 9 · I/O 강화
🚀 Phase 9 시작 — Stream 의 강화


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • 자원 (Resource) 의 정의와 수동 관리의 문제 는?
  • try-finally 의 4가지 구조적 문제는?
  • try-with-resources (Java 7+) 의 정확한 동작은?
  • AutoCloseable 인터페이스 의 역할과 Closeable 과의 차이는?
  • 컴파일러가 try-with-resources 를 어떻게 풀어내나 (디슈가링)?
  • Suppressed Exception 이 무엇이고 왜 도입되었나?
  • 자원의 close 순서 는 어떻게 결정되고 그 이유는?
  • Java 9 의 개선점 (effectively final 자원) 은?
  • 실무 활용 패턴 (DB, 파일, 네트워크, Stream) 은?

🎯 핵심 한 문장

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 = 자원 자동 관리.


🧭 9개 섹션 로드맵

1. 자원의 정의와 수동 관리의 문제
2. try-finally 의 4가지 구조적 문제
3. try-with-resources 의 동작
4. AutoCloseable 인터페이스
5. 컴파일러의 디슈가링
6. Suppressed Exception
7. 자원의 close 순서
8. Java 9+ 의 개선과 실무 패턴
9. 면접 + 자기 점검

1️⃣ 자원의 정의와 수동 관리의 문제

1.1 자원 (Resource) 의 정의

자원 (Resource):

  사용 후 명시적으로 해제해야 하는 객체.

종류:
  - 파일 핸들 (FileInputStream, FileOutputStream)
  - 네트워크 연결 (Socket, HttpURLConnection)
  - DB 연결 (Connection, Statement, ResultSet)
  - 스트림 (BufferedReader, BufferedWriter)
  - Lock (ReentrantLock 등)
  - 시스템 자원 (메모리 매핑된 버퍼)

1.2 자원 해제의 중요성

자원을 안 닫으면:

1. 파일 핸들 누수
   - OS 가 파일 핸들 한정 보유
   - 새 파일 못 열음 → "Too many open files"

2. DB 연결 누수
   - Connection Pool 고갈
   - 모든 요청 timeout

3. 메모리 누수
   - GC 가 정리 못 함 (외부 자원)
   - 메모리 점진 증가

4. 락 누수
   - 다른 스레드 영원히 대기
   - 데드락

1.3 Java 6 이전의 수동 관리

// 자원 해제 안 한 잘못된 코드
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() 실행 안 됨
// → 자원 누수!

1.4 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();   // 항상 실행
    }
}

// 보장:
// - 정상 흐름: 반환 후 close
// - 예외 흐름: 예외 전파 + close

1.5 다중 자원의 복잡함

// 두 자원 처리
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
// 자원 늘어날수록 코드 폭증
// 가독성 ↓↓

1.6 예외 마스킹 문제

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.7 수동 관리의 4가지 문제

1. 코드 장황함
   - 자원 1개에 try-finally 한 쌍
   - 여러 자원이면 중첩 폭증

2. 가독성 ↓
   - 본 로직과 자원 관리가 섞임
   - 핵심 코드가 가려짐

3. 누락 위험
   - close 호출 깜빡함
   - 자원 누수

4. 예외 마스킹
   - close 의 예외가 진짜 예외 가림
   - 디버깅 어려움

1.8 자기 점검 답변

자원의 수동 관리가 어려운 이유 4가지는?

:
1. 코드 장황함: try-finally 중첩으로 복잡
2. 가독성 ↓: 본 로직과 자원 관리 혼재
3. 누락 위험: close 깜빡함 → 자원 누수
4. 예외 마스킹: close 의 예외가 진짜 예외 가림

→ try-with-resources 가 모두 해결.


2️⃣ try-finally 의 4가지 구조적 문제

2.1 문제 1 — 단일 자원도 복잡

// 자원 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%

2.2 문제 2 — 다중 자원 폭증

// 자원 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줄

2.3 문제 3 — close 자체의 예외

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

2.4 문제 4 — 예외 마스킹 (Exception Masking)

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?"
// - 진짜 원인을 찾기 어려움

2.5 try-finally 의 예외 마스킹 시연

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" 의 진짜 원인을 못 봄

2.6 잘못된 close 누락 회피

// ❌ 잘못 — 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();
    }
}

2.7 try-finally 의 한계 종합

1. 코드 길이
   - 한 자원에 3-4줄 추가
   - 여러 자원이면 더

2. 중첩 복잡
   - 다중 자원 → 다중 중첩
   - 가독성 ↓

3. close 자체 예외
   - 별도 try-catch 필요

4. 예외 마스킹
   - 진짜 원인 숨겨짐
   - 디버깅 어려움

5. 인지 부담
   - 매번 패턴 작성
   - 깜빡함 위험

→ try-with-resources (Java 7+) 로 해결

2.8 자기 점검 답변

try-finally 의 4가지 구조적 문제는?

:
1. 코드 장황함: 1자원에 3-4줄 추가
2. 중첩 복잡: 다중 자원 시 코드 폭증
3. close 자체 예외: 별도 try-catch 필요
4. 예외 마스킹: close 의 예외가 진짜 원인 가림

→ try-with-resources 가 모두 해결.


3️⃣ try-with-resources 의 동작

3.1 기본 문법 (Java 7+)

// 단일 자원
public String firstLine(String path) throws IOException {
    try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
        return reader.readLine();
    }
}

핵심:

  • try ( ... ) 안에 자원 선언
  • 본문 끝나면 자동 close
  • catch, finally 도 가능

3.2 다중 자원

// 여러 자원 — 세미콜론으로 구분
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
}

3.3 catch/finally 와 결합

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 실행
    }
}

3.4 try-with-resources vs try-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. 가독성 ↑

3.5 ILIC 활용 예

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
    }
}

3.6 catch 절의 예외 흐름

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();
    }
}

3.7 자기 점검 답변

try-with-resources 의 기본 사용은?

:
1. 단일 자원:

try (Resource r = new Resource()) {
    // 사용
}
  1. 다중 자원:

    try (
        Resource1 r1 = ...;
        Resource2 r2 = ...
    ) {
        // 사용
    }
  2. catch/finally 결합:

    • close → catch → finally
    • 순서 명확

장점:

  • 자동 close
  • 예외 마스킹 해결
  • 가독성 ↑

4️⃣ AutoCloseable 인터페이스

4.1 AutoCloseable 의 정의

package java.lang;

public interface AutoCloseable {
    
    void close() throws Exception;
}

특징:

  • java.lang 패키지
  • 단일 메서드 close()
  • throws Exception (모든 예외)

4.2 Closeable 과의 차이

// 더 좁은 인터페이스
package java.io;

public interface Closeable extends AutoCloseable {
    
    @Override
    void close() throws IOException;   // ★ IOException 만
}

차이:

  • AutoCloseable: 모든 예외 가능
  • Closeable: IOException 만
  • Closeable 이 AutoCloseable 의 자식
계층:
  AutoCloseable (Java 7+, 모든 자원)
    └── Closeable (Java 5+, I/O 자원)

4.3 try-with-resources 의 자원 타입

// 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)

4.4 사용자 정의 AutoCloseable

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

4.5 AutoCloseable 의 두 시그니처

// 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() {   // 예외 없음 (가장 좁음)
        // ...
    }
}

4.6 javadoc 의 권장사항

AutoCloseable.close() 의 javadoc:

  "재진입 가능 (Idempotent) 하게 구현 권장"
  - 여러 번 호출해도 안전
  - 두 번째부터는 no-op

  "예외 발생 시 자원이 깨끗하게 정리되었는지 명확히"

  "InterruptedException 던지지 말기"
  - Thread 상태 영향

4.7 ILIC 적용 — Connection Pool 흉내

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 에 반환

4.8 자바 표준의 활용

// 모든 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()) { ... }

4.9 자기 점검 답변

AutoCloseable 과 Closeable 의 차이는?

:

  • AutoCloseable (java.lang, Java 7+):

    • 모든 자원
    • close() throws Exception
    • try-with-resources 의 본질
  • Closeable (java.io, Java 5+):

    • I/O 자원
    • close() throws IOException
    • AutoCloseable 의 자식

선택:

  • I/O 자원: Closeable (구체적)
  • 일반 자원: AutoCloseable (유연)
  • DB, Lock 등: AutoCloseable
  • 자바 표준은 적절히 구분

5️⃣ 컴파일러의 디슈가링

5.1 디슈가링 (Desugaring) 의 정의

디슈가링 (Desugaring):

  고수준 문법 → 저수준 문법 변환.
  
  컴파일러가 try-with-resources 를
  try-finally 코드로 변환.

5.2 단일 자원의 디슈가링

// 원본 (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();
        }
    }
}

핵심:

  • close 자동 호출
  • Suppressed Exception 으로 마스킹 해결
  • finally 가 명시적으로 생성됨

5.3 다중 자원의 디슈가링

// 원본
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.

5.4 close 순서

디슈가링의 흥미로운 면:

자원 선언 순서: r1, r2, r3
close 순서: r3, r2, r1 (역순!)

이유:
  - 자원 의존성 (r3 가 r2 의존, r2 가 r1 의존)
  - 마지막에 만들어진 게 먼저 닫혀야

비유:
  레고 조립 — 마지막 블록 먼저 분해

5.5 디슈가링 검증 (실제 바이트코드)

// 원본
public class Demo {
    public void test() throws IOException {
        try (FileInputStream in = new FileInputStream("test.txt")) {
            in.read();
        }
    }
}

// javac 컴파일 후 javap -c 로 보면:
// 실제로는 더 복잡한 바이트코드
// 하지만 개념적으로는 위의 try-finally 변환

5.6 Java 7 디슈가링 vs Java 8+ 디슈가링

Java 7:
  - 명시적 finally + 변수 추적
  - 비교적 복잡한 바이트코드

Java 8+:
  - 더 효율적인 바이트코드
  - JIT 최적화 잘됨

Java 11+:
  - 추가 최적화

5.7 디슈가링이 보여주는 메커니즘

try-with-resources 가 보장하는 것들:

1. close 자동 호출
   - try 블록 정상 종료 시
   - 예외 발생 시

2. 자원 변수가 null 인 경우 안전
   - if (resource != null) 체크

3. close 의 예외 처리
   - 정상 흐름: throw
   - 예외 흐름: addSuppressed

4. 다중 자원의 역순 close
   - 마지막 → 처음

5. catch 블록과의 정확한 순서
   - close → catch → finally

5.8 자기 점검 답변

컴파일러가 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();
        }
    }
}

6️⃣ Suppressed Exception

6.1 Suppressed Exception 의 정의

Suppressed Exception:

  주 예외 (primary) 가 발생한 후 추가로 발생한 예외.
  주 예외에 부가 정보로 첨부.

용도:
  - close 가 던진 예외가 try 의 예외를 가리는 문제 해결
  - 모든 예외 정보 보존

6.2 Throwable.addSuppressed 메서드

public class Throwable {
    
    private List<Throwable> suppressedExceptions;
    
    public final void addSuppressed(Throwable exception) {
        // 자기 자신에 부가 예외 추가
    }
    
    public final Throwable[] getSuppressed() {
        return suppressedExceptions.toArray(...);
    }
}

특징:

  • Throwable 의 메서드
  • Java 7+ 추가
  • try-with-resources 가 자동 호출

6.3 try-finally vs try-with-resources 의 예외 처리

// 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"]
    // 둘 다 보존!
}

6.4 예외 정보 출력

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
        }
    }
}

6.5 스택 트레이스의 출력

// 예외 출력 (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:" 표시
// 두 예외 모두 보임
// 디버깅 명확

6.6 Suppressed Exception 의 활용

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;
    }
}

6.7 다중 자원의 Suppressed

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

6.8 직접 addSuppressed 호출

// 직접 사용 (드물게)
RuntimeException primary = new RuntimeException("primary");
try {
    cleanup();
} catch (Exception cleanupEx) {
    primary.addSuppressed(cleanupEx);
}
throw primary;

// try-with-resources 는 자동

6.9 자기 점검 답변

Suppressed Exception 의 역할은?

:
1. 예외 마스킹 해결:

  • 주 예외가 close 의 예외에 가려지지 않음
  • 둘 다 보존
  1. try-with-resources 의 자동 처리:

    • 컴파일러가 addSuppressed 호출
    • 사용자 코드 단순
  2. 활용:

    • getSuppressed() 로 접근
    • 스택 트레이스에 "Suppressed:" 표시
    • 디버깅 명확
  3. 다중 자원:

    • 각 자원의 close 예외 모두 suppressed
    • 역순 close

7️⃣ 자원의 close 순서

7.1 close 의 역순

// 자원 선언 순서: 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()

7.2 역순의 이유

이유:

1. 의존성
   - r2 가 r1 의 자원 의존 가능
   - r1 먼저 닫으면 r2 의 close 실패 가능
   - r2 → r1 순서가 안전

2. LIFO (Last In, First Out)
   - 스택과 같은 원리
   - 마지막 생성 = 가장 의존성 적음
   - 먼저 닫혀도 안전

3. 자바 표준
   - DB: ResultSet → Statement → Connection
   - I/O: 외부 스트림 → 내부 스트림
   - 의존성 역방향

7.3 의존성 예 — DB

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 가 안전

7.4 I/O 의 예

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()

7.5 의존성 잘못된 순서의 위험

// ❌ 잘못된 선언 순서
try (
    PreparedStatement ps = conn.prepareStatement(sql);   // ❌ conn 먼저 필요
    Connection conn = dataSource.getConnection()
) {
    // 컴파일 에러
    // ps 선언 시 conn 이 아직 없음
}

// ✓ 올바른 순서
try (
    Connection conn = dataSource.getConnection();
    PreparedStatement ps = conn.prepareStatement(sql)
) {
    // ...
}

7.6 close 중 예외의 흐름

// 모든 자원이 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 막지 않음

7.7 자기 자원의 의존 관리

// 사용자 자원의 의존
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()

7.8 자기 점검 답변

다중 자원의 close 순서와 그 이유는?

:
1. 역순 (LIFO):

  • 마지막 선언 → 먼저 close
  • 처음 선언 → 나중 close
  1. 이유:

    • 의존성: 자식 자원이 부모 자원 의존
    • 안전: 부모 먼저 닫으면 자식 close 실패
    • 자바 표준: ResultSet → Statement → Connection
  2. :

    • DB: rs → ps → conn
    • I/O: dis → bis → fis (Decorator)
  3. 한 자원의 실패가 다른 자원에 영향 X:

    • 모두 close 시도
    • 예외는 suppressed

8️⃣ Java 9+ 의 개선과 실무 패턴

8.1 Java 9 — effectively final 자원

// 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+
        // ...
    }
}

8.2 effectively final 의 의미

effectively final:
  - 한 번 할당되고 다시 할당 안 됨
  - final 키워드 없어도 OK
  - 람다와 try-with-resources 가 활용

조건:
  - 변수 선언 후 한 번만 할당
  - 그 외 변경 없음

8.3 Java 9+ 활용

// 메서드 매개변수
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 후에 재할당 안 함

8.4 자바 표준의 활용

// 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 (...) { ... }

8.5 실무 패턴 1 — 파일 처리

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();
        }
    }
}

8.6 실무 패턴 2 — DB 작업

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();
        }
    }
}

8.7 실무 패턴 3 — 네트워크

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();
        }
    }
}

8.8 실무 패턴 4 — 락

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)) {
    // 임계 구역
}

8.9 ILIC 종합 적용

@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"));
            }
        }
        // 모든 자원 자동 정리
    }
}

8.10 자기 점검 답변

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) { ... }   // 직접 사용
    }
  • 효과:

    • 불필요한 새 변수 회피
    • 가독성 ↑
    • 코드 간결
  • 조건:

    • effectively final
    • 또는 final 명시

9️⃣ 면접 + 자기 점검

9.1 면접 단골 질문 매핑

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

9.2 자기 점검 체크리스트

기본 이해

  • 자원의 정의와 종류
  • 수동 관리의 위험성
  • try-finally 의 문제
  • try-with-resources 등장 시기 (Java 7+)

동작

  • 단일 자원 사용
  • 다중 자원 (세미콜론)
  • catch, finally 결합
  • 컴파일러 디슈가링

AutoCloseable

  • AutoCloseable 정의
  • Closeable 과 차이
  • 사용자 정의 구현
  • javadoc 권장사항

Suppressed Exception

  • 정의와 등장 배경
  • addSuppressed/getSuppressed
  • 스택 트레이스 출력
  • 활용

순서와 디슈가링

  • 역순 close 의 이유
  • LIFO 원리
  • 컴파일러의 처리
  • 다중 자원 시 중첩 finally

Java 9+ + 실무

  • effectively final 자원
  • DB 작업 패턴
  • 파일 처리 패턴
  • 네트워크 패턴
  • 락 패턴

9.3 추가 심화 질문

Q1: AutoCloseable 의 close 가 두 번 호출되면?

답:

  • javadoc 권장: idempotent (재진입 가능)
  • 첫 호출: 정상 동작
  • 둘째 호출: no-op 또는 IllegalStateException
  • 자바 표준 구현 (FileInputStream 등): 안전
public class IdempotentResource implements AutoCloseable {
    private boolean closed = false;
    
    @Override
    public void close() {
        if (closed) return;   // 두 번째 호출 무시
        closed = true;
        // 자원 해제
    }
}

Q2: try-with-resources 안에서 자원 변경?

답:

try (Resource r = ...) {
    r = newResource();   // ❌ 컴파일 에러 (final)
}
  • try-with-resources 의 자원은 implicitly final
  • 변경 불가

Q3: 자원이 null 이면?

답:

try (Resource r = mayBeNull()) {   // r 이 null 일 수도
    // 사용
}
// close 시: if (r != null) r.close();
// 컴파일러가 null 체크 자동 추가

Q4: 자원 생성 중 예외?

답:

try (
    Resource1 r1 = new Resource1();    // 성공
    Resource2 r2 = new Resource2()     // ❌ 예외
) {
    // 도달 못 함
}
// r1.close() 호출됨
// 예외는 호출자로 전파

Q5: try-with-resources 와 return 의 상호작용?

답:

public String test() {
    try (Resource r = new Resource()) {
        return "value";
    }
    // 흐름:
    // 1. "value" 평가
    // 2. r.close()
    // 3. "value" 반환
}

🎯 핵심 요약 — 3줄 정리

1. try-with-resources 등장

  • try-finally 의 4가지 문제 해결
  • 자동 close, Suppressed Exception
  • AutoCloseable 인터페이스

2. 동작 메커니즘

  • 컴파일러가 자동 try-finally + addSuppressed
  • 역순 close (LIFO)
  • catch/finally 와 자연스러운 결합

3. 실무 활용

  • DB 작업 (Connection/Statement/ResultSet)
  • 파일 처리 (BufferedReader/Writer)
  • 네트워크, 락, Stream
  • Java 9+ effectively final

📚 다음으로...

Unit 9.2 — BufferedInputStream / BufferedOutputStream

이번 Unit에서 자원 관리 (try-with-resources) 를 봤다면, 다음은 버퍼링 스트림의 정밀.

  • BufferedInputStream 의 8KB 버퍼
  • BufferedOutputStream 의 flush 정밀
  • 일반 Stream 과의 성능 비교
  • Decorator 패턴의 정수

Phase 9 진행 상황

🚀 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

3주차 누적 진행

✅ 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 의 강화 진입

profile
Software Developer

0개의 댓글