인턴

try-with-resourece를 사용해야 하는 이유

jonghyeon6084 2025. 7. 3. 11:03
728x90

1차 과제에서 2번째로 받았던 피드백이다. 파일 업로드를 할 때 try-catch로 예외처리를 했는데

 

  • 왜 IO/스트림은 사용 후 반드시 close()를 호출해야 하는가?
  • 왜 try-catch-finally 대신, try(…){…} (try-with-resources) 구문을 쓰는 게 더 좋은가?

에 대해 알아보자.

1. IO/스트림 사용 후 반드시 close()를 해줘야 하는 이유

1.1 스트림(stream)이란?

  • 스트림은 파일, 네트워크, 메모리 등으로부터 데이터를 읽고 쓰는 연결 통로이다.
  • 예: FileInputStream, BufferedReader, Socket, OutputStream, FileWriter, 등

1.2 왜 close()가 필요한가?

리소스(자원)란?

  • OS 레벨에서 파일 핸들, 네트워크 소켓, 메모리 버퍼 등은 "한정된 리소스"이다.
  • 이런 리소스는 프로그램에서 명확히 닫아주지 않으면 계속 점유되고,
    • 파일: 파일을 연 채로 남음 → 다른 프로그램/프로세스가 해당 파일을 쓰거나 지우지 못함
    • 네트워크: 소켓이 닫히지 않아 포트가 계속 열려 있음
    • 메모리: 버퍼가 해제되지 않아 메모리 누수 발생
  • **자바의 GC(가비지 컬렉터)**는 객체는 자동으로 치워주지만,
    OS 리소스(파일 핸들, 네트워크 연결)는 직접 close()를 호출하지 않으면 즉시 해제되지 않음.

예시 : 파일을 안 닫는 경우

  • windows에서 파일을 연 뒤 close()를 안하면, 나중에 삭제/이동/다른 앱에서 오픈이 안 될 수 있음.
  • 서버에서 스트림을 계속 안 닫으면, "열린 파일 핸들 수 초과"로 전체 서버가 다운될 수도 있음.

1.3 close()는 언제 호출하는가?

  • IO, 네트워크, DB 등 "외부 리소스"를 다루는 모든 경우
    • 파일, DB 커넥션, 소켓 등
  • 파일/스트림 사용 후 마지막에 반드시 호출해야 함
  • 예외가 발생해도 꼭 닫아야 함 → 그래서 finally에서 close()를 써 왔던 것

2. 전통적인 try-catch-finally의 문제점

2.1 기본 패턴

InputStream in = null;
try {
    in = new FileInputStream("test.txt");
    // ... 데이터 읽기
} catch(IOException e) {
    // 예외 처리
} finally {
    if (in != null) {
        try {
            in.close();
        } catch(IOException e) {
            // close 도중의 예외도 처리
        }
    }
}

 

단점

  • 가독성 떨어짐 : finally 블록에서 null 체크/중첩 try-catch 필수
  • 코드가 길어짐 : 모든 IO객체마다 close()를 일일히 써야 함
  • 실수 위험 : 여러 리소스를 쓸 때 닫는 순서, 중복 처리 등 실수하기 쉽다.

3. try-with-resources가 등장한 이유 (Java 7+)

3.1 try-with-resources란?

  • try(리소스 선언) {...}
  • try문 바로 뒤 괄호() 안에서 AutoCloseable 인터페이스를 구현한 객체를 선언하면,
    try 블록이 끝날 때 자동으로 close() 호출

기본 구조

try (InputStream in = new FileInputStream("test.txt")) {
    // ... 데이터 읽기
} catch (IOException e) {
    // 예외 처리
}
// finally, 자동으로 in.close() 호출됨!
  • 여러 개의 리소스도 한번 처리
try (
    InputStream in = new FileInputStream("test.txt");
    OutputStream out = new FileOutputStream("copy.txt");
) {
    // 파일 복사
}
// 두 객체 모두 자동으로 close()

3.2 내부 동작 원리

 

  • try() 괄호 안에 선언된 객체가 AutoCloseable (혹은 옛날 Closeable) 인터페이스를 구현해야 함
  • try 블록을 빠져나가는 모든 경우(정상 종료, 예외 발생, return 등)
    자동으로 .close()가 호출
  • close() 중 예외가 발생해도 안전하게 예외 처리가 가능하도록 설계됨

3.3 왜 try-with-resourecs가 좋은가?

장점 1: 실수 방지(누수 방지)

 

  • finally에 close()를 안 적어도, 무조건 닫아줌
  • 예외가 발생해도 안전하게 닫힘

장점 2: 코드 간결성

  • 더 이상 중첩 try-catch/finally 블록 쓸 필요 없음

장점 3: 여러 리소스 안전하게 처리

  • 여러 리소스를 선언해도, 닫는 순서까지 알아서 관리 (역순으로 close() 호출: 마지막에 선언한 것부터 닫음)

장점 4: 예외 정보 추적이 쉬움

  • try-with-resources는 close() 중 발생한 예외도 **suppressed exception으로 남겨주어, 문제 추적이 용이

5. JVM, Spring, 그리고 GC와의 관계

  • GC는 자바 객체만 관리
    → OS 레벨의 자원(파일, 네트워크, DB)은 close()를 통해 직접 명시적으로 해제해줘야 함
  • Spring의 @PreDestroy 등도 있지만, 가장 안전한 건 직접 닫기

 

 

--------------------------------------------------------------------------

Suppressed Exception(억제된 예외)이란?

1. 문제의 배경

  • try 블록 안에서 예외가 발생하면 catch/finally로 이동
  • 만약 close() 중에 또 다른 예외가 발생하면, 원래 예외와 close() 예외 중 하나만 표면적으로 남게 됨
    (전통적 try-catch-finally 패턴의 고질적 문제)

2. Suppressed Exception의 역할

  • try-with-resources에서는 try 블록에서 발생한 예외가 “주 예외”로 남고,
    close() 중 발생한 예외는 “suppressed(억제된)” 예외로 저장
    됩니다.
  • 이렇게 하면 모든 예외 정보를 잃지 않고 추적 가능하게 됩니다.

3. Suppressed Exception 예시

class MyResource implements AutoCloseable {
    @Override
    public void close() throws Exception {
        throw new Exception("예외: close()에서 발생");
    }
}
try (MyResource r = new MyResource()) {
    throw new Exception("예외: try 블록 안에서 발생");
} catch (Exception e) {
    System.out.println("주 예외: " + e.getMessage());
    for (Throwable t : e.getSuppressed()) {
        System.out.println("Suppressed: " + t.getMessage());
    }
}

 

 

출력 : 

주 예외: 예외: try 블록 안에서 발생
Suppressed: 예외: close()에서 발생

→ 즉, 모든 예외를 추적할 수 있음!