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 인터페이스를 구현하는 모든 클래스입니다. 표준 클래스 몇 가지만 예로 들면:
| 클래스 | 용도 |
|---|---|
|
파일에서 바이트 읽기 |
|
파일에 바이트 쓰기 |
|
텍스트 읽기/쓰기 |
|
스트림 버퍼링 |
|
서식 있는 텍스트 쓰기 |
|
파일/콘솔에서 데이터 읽기 |
|
직렬화/역직렬화 |
|
ZIP 아카이브 작업 |
|
네트워크 연결 |
타사 라이브러리를 사용한다면 문서를 확인하세요. 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()를 잊지 마세요.
GO TO FULL VERSION