CodeGym /행동 /JAVA 25 SELF /try-with-resources 사용: 리소스 자동 닫기

try-with-resources 사용: 리소스 자동 닫기

JAVA 25 SELF
레벨 36 , 레슨 4
사용 가능

1. 소개

Java에서 파일 작업(그리고 그 외의 많은 것들)은 항상 외부 리소스와의 작업입니다. 파일을 열면 운영체제가 프로그램을 위해 “디스크립터” — 파일을 읽고 쓸 수 있게 하는 특별한 식별자를 할당합니다. 이러한 디스크립터의 수는 제한되어 있습니다. 파일을 닫지 않으면 프로그램이 사용 가능한 리소스를 빠르게 모두 “잡아먹고” "Too many open files" 같은 난해한 오류를 던지기 시작할 수 있습니다.

게다가 파일을 닫지 않으면 다른 프로그램에 대해 잠긴 상태로 남을 수 있습니다. 예를 들어, 파일을 쓰기 모드로 열어 두고 닫는 것을 잊어버리면, 이제 당신도, 다른 누구도 그 파일을 변경하거나 삭제할 수 없습니다. 파일 시스템 세계의 “영원한 인질극” 같은 상황이죠.

현실 세계의 예

FileInputStream fis = new FileInputStream("data.txt");
int b = fis.read();
// ... 뭔가를 하고 나서 fis.close()를 잊어버림

close() 메서드를 호출하지 않으면 파일은 프로그램이 종료될 때까지 “매달린” 상태로 남습니다. 대형 애플리케이션에서는 리소스 누수로 이어지고 심지어 애플리케이션이 다운될 수도 있습니다.

2. 옛 방식: finally + close()

Java 7 이전에 파일을 확실히 닫는 고전적인 방법은 다음과 같았습니다:

FileInputStream fis = null;
try {
    fis = new FileInputStream("data.txt");
    // 파일을 읽는다
    int b = fis.read();
    // ...
} catch (IOException e) {
    System.out.println("파일 읽기 오류: " + e.getMessage());
} finally {
    if (fis != null) {
        try {
            fis.close();
        } catch (IOException e) {
            System.out.println("파일 닫기 오류: " + e.getMessage());
        }
    }
}

이 접근법의 단점

  • finally 블록을 깜빡하기 쉽고, 그러면 리소스 누수가 발생합니다.
  • 특히 스트림이 여러 개일 때 불필요한 코드가 많습니다.
  • 닫는 동안 예외가 발생하면 그것도 별도로 잡아야 합니다.
  • 코드가 장황하고 가독성이 떨어집니다.

3. 현대적 접근: try-with-resources

다행히 Java 7에서 이러한 문제를 우아하고 자동으로 해결하는 구문 — try-with-resources가 도입되었습니다.

모양은 이렇게

try (FileInputStream fis = new FileInputStream("data.txt")) {
    int b = fis.read();
    // 파일을 다룬다
} catch (IOException e) {
    System.out.println("파일 처리 중 오류: " + e.getMessage());
}
// 여기서는 fis가 이미 자동으로 닫혔다!

핵심 포인트: try 뒤의 괄호 안에 선언된 모든 리소스는 블록이 끝난 후 자동으로 닫힙니다 — 중간에 예외가 발생하더라도요. finally를 작성할 필요도, 닫는 과정에서의 오류를 별도로 잡을 필요도 없습니다 — Java가 모두 처리해 줍니다.

try-with-resources에 사용할 수 있는 클래스는?

AutoCloseable(또는 예전의 Closeable) 인터페이스를 구현한 클래스라면 무엇이든 가능합니다. 표준 입출력 스트림 대부분이 여기에 해당합니다: FileInputStream, FileOutputStream, BufferedReader, BufferedWriter, Scanner, PrintWriter 등.

4. try-with-resources 구문: 디테일과 예제

리소스 하나

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    System.out.println("오류: " + e.getMessage());
}
// reader는 자동으로 닫힌다!

여러 리소스

여러 리소스를 세미콜론으로 한 번에 선언할 수 있습니다:

try (
    BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
    BufferedWriter writer = new BufferedWriter(new FileWriter("output.txt"))
) {
    String line;
    while ((line = reader.readLine()) != null) {
        writer.write(line);
        writer.newLine();
    }
} catch (IOException e) {
    System.out.println("복사 중 오류: " + e.getMessage());
}
// 두 스트림 모두 닫혔다!

닫는 순서: 리소스는 선언의 역순으로 닫힙니다. 먼저 writer.close()가 호출되고, 그다음 reader.close()가 호출됩니다. 한 스트림이 다른 스트림에 의존하는 경우 중요합니다.

사용자 정의 클래스와 함께 사용하기

리소스를 다루는 사용자 정의 클래스를 작성한다면, AutoCloseable 인터페이스를 구현하면 됩니다:

class MyResource implements AutoCloseable {
    public void doSomething() {
        System.out.println("리소스로 작업 중!");
    }

    @Override
    public void close() {
        System.out.println("리소스가 닫혔습니다!");
    }
}

try (MyResource res = new MyResource()) {
    res.doSomething();
}
// 블록을 빠져나온 후에 출력됨: "리소스가 닫혔습니다!"

5. 어떻게 동작하나: 개요

flowchart TD
    A[try-with-resources에서 리소스 열기] --> B{try 블록에서 예외가 발생했는가?}
    B -- 아니오 --> C[리소스가 자동으로 닫힘]
    B -- 예 --> D[리소스가 자동으로 닫힘]
    D --> E[예외가 상위로 전파됨]
    C --> F[프로그램이 계속 실행됨]

결론: 오류가 있었든 없었든 리소스는 항상 닫힙니다!

6. 예제: 코드를 “요즘 방식”으로 다시 쓰기

이전(구식) 코드:

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("input.txt"));
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    System.out.println("오류: " + e.getMessage());
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            System.out.println("닫는 중 오류: " + e.getMessage());
        }
    }
}

변경(try-with-resources):

try (BufferedReader reader = new BufferedReader(new FileReader("input.txt"))) {
    String line = reader.readLine();
    System.out.println(line);
} catch (IOException e) {
    System.out.println("오류: " + e.getMessage());
}
// 끝. finally는 필요 없다!

7. 닫는 동안 오류가 발생하면 어떻게 될까?

때때로 리소스를 닫는 작업 자체가 예외를 던질 수 있습니다(예: 파일이 갑자기 사라진 경우). try-with-resources에서는 이러한 예외가 사라지지 않습니다. try 블록에서 이미 하나의 예외가 발생했고 리소스를 닫는 동안 두 번째 예외가 발생하면, 그 예외는 기본 예외에 “억제된(suppressed)” 예외로 추가됩니다. 이는 Throwable.getSuppressed() 메서드로 확인할 수 있습니다.

예제

try (MyResource res = new MyResource()) {
    throw new IOException("try 블록에서 오류");
} catch (IOException e) {
    System.out.println("주요 오류: " + e.getMessage());
    for (Throwable suppressed : e.getSuppressed()) {
        System.out.println("억제된 예외: " + suppressed.getMessage());
    }
}

8. 어떤 클래스들이 try-with-resources를 지원하나요?

아주 간단합니다. AutoCloseable 인터페이스를 구현하는 모든 클래스입니다. 표준 클래스 몇 가지만 예로 들면:

클래스 용도
FileInputStream
파일에서 바이트 읽기
FileOutputStream
파일에 바이트 쓰기
FileReader/FileWriter
텍스트 읽기/쓰기
BufferedReader/Writer
스트림 버퍼링
PrintWriter
서식 있는 텍스트 쓰기
Scanner
파일/콘솔에서 데이터 읽기
ObjectInputStream/Output
직렬화/역직렬화
ZipInputStream/Output
ZIP 아카이브 작업
Socket, ServerSocket
네트워크 연결

타사 라이브러리를 사용한다면 문서를 확인하세요. close() 메서드가 있으면 대체로 try-with-resources를 지원합니다.

9. 팁과 유용한 포인트

try 밖에서 변수 선언 가능(Java 9 이상): 이미 선언된 리소스를 사용할 수 있습니다. 단, 해당 리소스는 final 또는 “사실상 final(effectively final)”이어야 합니다:

BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try (reader) {
    // ...
}

파일에만 국한되지 않음: try-with-resources는 네트워크 연결, 데이터베이스, close() 메서드가 있는 모든 객체 등 어떤 리소스에도 유용합니다.

예외를 무시하지 마세요: try-with-resources를 사용하더라도 예외를 잡고 처리하는 것을 잊지 마세요. 만능 해결책이 아니라 리소스 누수를 방지하는 편리한 방법일 뿐입니다.

try 블록 내부에서 리소스를 수동으로 닫지 마세요: 그럴 필요가 없습니다 — Java가 알아서 처리합니다! close()를 수동으로 호출한 뒤 try 블록이 끝나면 이미 닫힌 리소스를 다시 닫으려 할 것입니다. 보통은 안전하지만 혼란을 줄 수 있습니다.

10. try-with-resources 사용 시 흔한 실수

실수 №1: 아예 try-with-resources를 사용하지 않음. 여전히 finally { resource.close(); }를 작성하고 있다면 — 2011년에 머물러 있거나 이 강의를 아직 읽지 않은 것입니다! 현대적인 구문을 사용하세요.

실수 №2: 리소스를 try 밖에서 선언하고, 안에서는 그냥 사용함. 이런 코드는 리소스를 자동으로 닫지 않습니다:

BufferedReader reader = new BufferedReader(new FileReader("input.txt"));
try {
    // ... reader를 사용
} finally {
    // 여기서 닫는 것을 잊었다!
}

실수 №3: try 블록 안에서 close()를 직접 호출함. 치명적이지는 않지만 중복이며 이중 닫기로 이어질 수 있습니다. Java를 믿으세요.

실수 №4: IO 특성을 무시하고 Exception만 잡음. 사용자에게 이해하기 쉬운 메시지를 제공하려면 FileNotFoundException, IOException 등 구체적인 예외를 잡는 것이 좋습니다.

실수 №5: 억제된 예외를 처리하지 않음. 리소스를 닫는 동안 오류가 발생하면 그것이 “억제”될 수 있습니다. 오류를 분석할 때 getSuppressed()를 잊지 마세요.

1
과제
JAVA 25 SELF, 레벨 36, 레슨 4
잠금
마법 각인이 새겨진 돌에 메시지 새기기 ✨
마법 각인이 새겨진 돌에 메시지 새기기 ✨
1
과제
JAVA 25 SELF, 레벨 36, 레슨 4
잠금
도서관의 고대 원고 단어 수 세기 📚
도서관의 고대 원고 단어 수 세기 📚
1
설문조사/퀴즈
파일 읽기와 쓰기, 레벨 36, 레슨 4
사용 불가능
파일 읽기와 쓰기
파일 읽기와 쓰기
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION