8.4 try-with-resources와 예외 고급 기법
자원을 사용한 후 반드시 닫아야 하는 상황에서 발생하는 번거로움과 실수를 한 번에 해결하는 Java 7의 핵심 기능 try-with-resources를 배웁니다.
1. 자원 관리의 문제점
파일, 데이터베이스 연결, 네트워크 소켓 등은 사용 후 반드시 닫아야(close) 합니다. 기존 방식은 항상 finally에서 닫아야 했고, 실수하기 쉬웠습니다.
// ❌ 기존 방식 - 번거롭고 실수하기 쉬움
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader("data.txt"));
String line = reader.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) { // null 체크 필수
try {
reader.close(); // close() 자체도 예외가 날 수 있음!
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. try-with-resources (Java 7+)
try ( ... ) 괄호 안에 자원을 선언하면, try 블록이 끝날 때 자동으로 close()를 호출합니다. 정상 종료든 예외 발생이든 상관없이 항상 닫힙니다.
조건: 자원 클래스가
java.lang.AutoCloseable(또는java.io.Closeable) 인터페이스를 구현해야 합니다.
// ✅ try-with-resources - 간결하고 안전!
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.out.println("파일 읽기 오류: " + e.getMessage());
}
// reader.close()는 자동 호출! finally 블록 불필요
여러 자원 동시 관리
// 세미콜론으로 여러 자원 선언 (역순으로 닫힘: out → in)
try (
FileInputStream in = new FileInputStream("source.txt");
FileOutputStream out = new FileOutputStream("dest.txt")
) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
System.out.println("파일 복사 완료!");
} catch (IOException e) {
e.printStackTrace();
}
커스텀 AutoCloseable 구현
class DatabaseConnection implements AutoCloseable {
private final String url;
DatabaseConnection(String url) {
this.url = url;
System.out.println("DB 연결: " + url);
}
void query(String sql) {
System.out.println("쿼리 실행: " + sql);
}
@Override
public void close() {
System.out.println("DB 연결 해제: " + url); // 자동 호출됨
}
}
// 사용
try (DatabaseConnection conn = new DatabaseConnection("jdbc:mysql://localhost/shop")) {
conn.query("SELECT * FROM users");
} // 여기서 conn.close() 자동 호출
// 출력:
// DB 연결: jdbc:mysql://localhost/shop
// 쿼리 실행: SELECT * FROM users
// DB 연결 해제: jdbc:mysql://localhost/shop
3. Multi-catch (여러 예외를 하나로)
Java 7+에서는 | 연산자로 여러 예외를 하나의 catch에서 처리할 수 있습니다.
// ❌ 기존: 같은 처리인데 catch 블록 반복
try {
// ...
} catch (FileNotFoundException e) {
System.out.println("오류: " + e.getMessage());
e.printStackTrace();
} catch (ParseException e) {
System.out.println("오류: " + e.getMessage());
e.printStackTrace();
}
// ✅ Multi-catch: | 로 묶기
try {
String data = readFile("config.txt");
int value = Integer.parseInt(data);
} catch (IOException | NumberFormatException e) {
// 두 예외를 하나로 처리
System.out.println("데이터 처리 오류: " + e.getMessage());
e.printStackTrace();
}
주의: Multi-catch에서
e는final이므로 재할당할 수 없습니다.
4. 예외 체이닝 (Exception Chaining)
예외를 잡아서 다른 예외로 감쌀 때 원인 예외를 보존합니다. 스택 트레이스에서 원인을 추적할 수 있습니다.
class DataService {
void loadData(String path) throws DataLoadException {
try {
// 실제 파일 읽기 시도
new FileReader(path);
} catch (FileNotFoundException e) {
// 저수준 예외를 고수준 예외로 감싸기 (원인 보존)
throw new DataLoadException("데이터 파일을 찾을 수 없음: " + path, e);
}
}
}
class DataLoadException extends Exception {
DataLoadException(String message, Throwable cause) {
super(message, cause); // cause: 원인 예외를 함께 전달
}
}
// 사용
try {
new DataService().loadData("missing.csv");
} catch (DataLoadException e) {
System.out.println("고수준 오류: " + e.getMessage());
System.out.println("원인: " + e.getCause().getMessage()); // FileNotFoundException
e.printStackTrace(); // 전체 예외 체인 출력
}
5. Checked vs Unchecked 예외 정리
| 구분 | 예시 | 처리 방식 |
|---|---|---|
| Checked 예외 | IOException, SQLException | 반드시 try-catch 또는 throws 선언 |
| Unchecked 예외 | NullPointerException, IllegalArgumentException | 선택적 처리 (런타임 예외) |
| Error | OutOfMemoryError, StackOverflowError | 일반적으로 처리 불필요 |
// Checked 예외: 컴파일러가 처리를 강제
void readFile(String path) throws IOException { // throws 선언 필수
new FileReader(path);
}
// Unchecked 예외: 처리 선택적 (방어적 프로그래밍 권장)
void divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("나눗수는 0이 될 수 없습니다.");
return a / b;
}
실무 예외 처리 패턴:
-
Checked → Unchecked 변환: 라이브러리 메서드의 Checked 예외를 API 경계에서 Unchecked로 감싸면, 호출자가 매번 try-catch를 강요받지 않아 코드가 깔끔해집니다.
// Checked를 RuntimeException으로 포장
static String readFileSafely(String path) {
try { return Files.readString(Path.of(path)); }
catch (IOException e) { throw new RuntimeException("파일 읽기 실패: " + path, e); }
} -
예외 메시지에 컨텍스트 정보 포함: "파일을 찾을 수 없습니다"보다 "사용자 ID 123의 프로필 이미지(/uploads/123.jpg)를 찾을 수 없습니다"가 디버깅에 훨씬 유용합니다.
-
try-with-resources를 람다로 추상화 (헬퍼 패턴):
@FunctionalInterface
interface ThrowingSupplier<T> {
T get() throws Exception;
}
static <T> T withDb(String url, ThrowingSupplier<T> action) {
try (var conn = new DatabaseConnection(url)) {
return action.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 사용
String result = withDb("jdbc:...", () -> {
// DB 작업
return "결과";
});