본문으로 건너뛰기
Advertisement

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에서 efinal이므로 재할당할 수 없습니다.


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선택적 처리 (런타임 예외)
ErrorOutOfMemoryError, 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;
}

고수 팁

실무 예외 처리 패턴:

  1. 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); }
    }
  2. 예외 메시지에 컨텍스트 정보 포함: "파일을 찾을 수 없습니다"보다 "사용자 ID 123의 프로필 이미지(/uploads/123.jpg)를 찾을 수 없습니다"가 디버깅에 훨씬 유용합니다.

  3. 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 "결과";
    });
Advertisement