본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.
자바 예외 처리 심화 학습: try-with-resources와 throws로 코드 다이어트 & 책임 전가!

안녕하세요 여러분!
지난 시간에는 try-catch-finally 삼총사를 배우면서 예상치 못한 예외 상황에서도 우리 프로그램을 튼튼하게 지키는 방법을 익혔죠! 🛡️🚦
마치 갑작스러운 비상 상황에도 침착하게 대처하는 베테랑 소방관처럼, 예외를 잡고(catch), 반드시 처리해야 할 뒷정리(finally)까지 깔끔하게 마무리하는 법을 배웠습니다. 덕분에 프로그램이 갑자기 멈춰 사용자를 당황시키는 일을 막을 수 있게 되었어요! 👍
하지만 여기서 멈출 순 없죠! 😉 자바는 우리가 코드를 더 간결하고, 책임 소재를 명확하게 작성할 수 있도록 도와주는 멋진 기능들을 더 가지고 있답니다!
특히, finally 블록에서 자원을 해제하는 코드는 매우 중요하지만, 매번 작성하기에는 조금 번거롭게 느껴질 때도 있었을 거예요. 그리고 때로는, "이 예외는 내가 여기서 처리하는 것보다 나를 호출한 쪽에서 처리하는 게 더 맞을 것 같은데?" 라는 생각이 들 때도 있죠. 🤔
그래서 오늘은! 바로 이런 고민들을 해결해 줄 두 가지 강력한 기능을 배워볼 거예요!
- try-with-resources: 자원 관리를 더욱 스마트하고 간결하게! 🤖✨
- throws: 예외 처리의 책임을 명확하게 전달하기! 🙋♀️➡️🙋♂️
자, 준비되셨나요? 더 세련된 예외 처리의 세계로 함께 떠나봅시다! 🚀

1. try-with-resources: 자원 관리, 이제 자동으로 맡겨주세요! ✨🧹➡️🤖
지난 시간에 finally 블록의 중요한 역할 중 하나가 사용한 자원(파일 스트림, 네트워크 연결 등)을 안전하게 닫아주는 것이라고 배웠어요. finally는 예외 발생 여부와 상관없이 항상 실행되니 자원 누수를 막는 데 아주 효과적이죠!
하지만 Scanner나 FileOutputStream 같은 자원을 사용할 때마다 try-catch-finally 구조를 다 갖춰서 close() 메소드를 명시적으로 호출하는 것이 때로는 코드를 길고 복잡하게 만들기도 해요. 😅
// 기존 방식 (finally에서 자원 해제)
FileOutputStream out = null; // try 밖에서 선언 필요
try {
out = new FileOutputStream("old_test.txt");
out.write("Hello Old Way!".getBytes());
out.flush();
} catch (IOException e) {
System.err.println("파일 쓰기 중 오류 발생: " + e.getMessage());
} finally {
System.out.println("--- finally 블록 (기존 방식) ---");
if (out != null) {
try {
System.out.println("FileOutputStream 자원을 닫습니다.");
out.close(); // 명시적으로 close() 호출!
} catch (IOException e) {
System.err.println("자원 닫기 중 오류 발생: " + e.getMessage());
// close() 자체도 IOException을 던질 수 있어서 또 try-catch가 필요할 수도... 복잡! 🤯
}
}
}
보세요! finally 안에서 close()를 호출하는 것 자체도 예외가 발생할 수 있어서 코드가 더 복잡해질 수 있어요. 😱
바로 이때! 구세주처럼 등장하는 것이 try-with-resources 문법입니다! 🎉
try 키워드 뒤에 오는 괄호 () 안에 자원 객체를 생성하는 코드를 넣어주면, try 블록이 끝나거나 예외가 발생해서 블록을 벗어나는 순간! 자바가 자동으로 해당 자원의 close() 메소드를 호출해줍니다! 띠용! 👀
// try-with-resources 방식 ✨
try (FileOutputStream out = new FileOutputStream("new_test.txt")) { // try() 괄호 안에 자원 생성!
// test.txt 파일에 Hello New Way! 를 출력
System.out.println("try 블록 시작!");
out.write("Hello New Way!".getBytes());
out.flush();
System.out.println("파일 쓰기 완료!");
// try 블록이 끝나면 자동으로 out.close()가 호출됨!
} catch (IOException e) {
System.err.println("🚨 파일 관련 오류 발생: " + e.getMessage());
}
// finally 블록이 없어도 자원은 안전하게 닫힙니다! 😄
System.out.println("--- try-with-resources 이후 ---");
핵심:
- try (...): 괄호 안에 자원 객체를 생성합니다. (세미콜론으로 구분하여 여러 개 선언 가능!)
- 자동 close() 호출: try 블록을 벗어나면 (정상 종료든, 예외 발생이든) 괄호 안에서 생성된 자원의 close() 메소드가 자동으로 호출됩니다.
- 조건: 이 기능을 사용하려면 해당 자원 클래스가 AutoCloseable 인터페이스를 구현하고 있어야 해요. (다행히 InputStream, OutputStream, Scanner, Connection 등 대부분의 표준 자원 클래스들이 이를 구현하고 있답니다!)
어때요? finally 블록에서 close()를 직접 호출하고 null 체크를 하던 코드가 훨씬 깔끔하고 안전해졌죠? 마치 설거지거리를 싱크대에 넣기만 하면 식기세척기가 알아서 씻어주는 느낌이랄까요? 🍽️➡️🤖 정말 편리하죠!
왜 AutoCloseable일까요? 🤔
이 인터페이스에는 close() 메소드가 정의되어 있기 때문이에요. try-with-resources 구문은 try 블록이 끝날 때 이 인터페이스를 구현한 객체의 close() 메소드를 자동으로 호출하도록 약속되어 있습니다.
2. throws: "저는 이 예외 처리 못 해요! 호출한 분이 처리해주세요!" 🙋♀️➡️🙋♂️

try-catch를 사용하면 발생한 예외를 그 자리에서 직접 처리할 수 있었죠. 그런데 때로는 어떤 메소드 안에서 예외가 발생할 수는 있지만, 그 예외를 어떻게 처리하는 것이 최선인지 해당 메소드 스스로는 결정하기 어려울 때가 있어요. 오히려 그 메소드를 호출한 쪽(caller)에서 예외 상황에 대한 정책을 결정하고 처리하는 것이 더 적합할 수 있습니다.
예를 들어, 파일을 읽는 메소드는 파일이 없을 때 FileNotFoundException을 발생시킬 수 있지만, "파일이 없을 경우 기본값을 사용해야 할지", "사용자에게 다른 파일을 선택하라고 안내해야 할지", "오류 로그만 남겨야 할지"는 파일을 읽는 메소드 자체보다는 그 메소드를 사용하는 상위 로직에서 결정하는 것이 더 자연스러울 수 있죠.
이럴 때 사용하는 것이 바로 throws 키워드입니다! 💨
메소드 선언부 마지막에 throws 키워드와 함께 이 메소드 내에서 발생할 수 있으며, 여기서 직접 처리하지 않고 호출한 쪽으로 '던질(throw)' 예외의 종류를 명시해주는 거예요.
import java.io.IOException;
public class ExceptionThrower {
// 이 메소드는 IOException이 발생할 수 있으며,
// 여기서 직접 처리하지 않고 호출한 곳으로 던지겠다고 선언!
void readFile(String filePath) throws IOException {
System.out.println(filePath + " 파일을 읽으려고 시도합니다...");
// 파일을 읽는 로직 (여기서 IOException 발생 가능성 있음)
if (filePath.equals("nonexistent.txt")) {
throw new IOException("'" + filePath + "' 파일을 찾을 수 없습니다!"); // 예외를 실제로 던짐
}
System.out.println(filePath + " 파일 읽기 성공! (가정)");
}
// 이 메소드는 RuntimeException이나 그 하위 예외를 던질 수 있다고 선언할 수도 있지만,
// RuntimeException 계열(Unchecked Exception)은 throws 선언이 필수는 아님!
void mightThrowRuntimeException() throws RuntimeException {
System.out.println("RuntimeException 발생 가능성이 있는 메소드");
if (true) { // 간단한 예시를 위해 무조건 발생
throw new NullPointerException("가상의 Null 참조 발생!");
}
}
// readFile()을 호출하는 메소드
void callerMethod() {
System.out.println("callerMethod 시작!");
ExceptionThrower thrower = new ExceptionThrower();
// readFile()은 IOException을 throws 하고 있으므로,
// 호출하는 쪽에서는 반드시 try-catch로 처리하거나,
// 또는 callerMethod() 자신도 throws IOException을 선언해야 함! (컴파일 에러 방지)
try {
thrower.readFile("my_document.txt"); // 성공 시나리오
thrower.readFile("nonexistent.txt"); // 예외 발생 시나리오
} catch (IOException e) {
System.err.println("🚨 callerMethod에서 파일 읽기 예외 처리: " + e.getMessage());
// 여기서 사용자에게 알림을 주거나, 대체 로직을 수행할 수 있음
}
// mightThrowRuntimeException()은 RuntimeException 계열을 던지지만,
// throws 선언이 있더라도 호출 시 try-catch가 강제되지 않음 (Unchecked Exception의 특징)
// 하지만, 예외 발생 가능성을 인지하고 처리해주는 것이 좋음!
try {
thrower.mightThrowRuntimeException();
} catch (NullPointerException e) {
System.err.println("🚨 callerMethod에서 NPE 처리: " + e.getMessage());
}
System.out.println("callerMethod 종료!");
}
public static void main(String[] args) {
new ExceptionThrower().callerMethod();
}
}
핵심 정리:
- void myMethod() throws SomeCheckedException { ... }: 메소드 선언부에 throws와 예외 클래스명을 적어, 해당 종류의 Checked Exception이 발생할 수 있으며 이 메소드에서는 처리하지 않음을 알립니다. (Checked Exception: Exception을 상속받지만 RuntimeException 계열이 아닌 예외들. 컴파일러가 처리를 강제함)
- 호출자의 의무: throws로 예외가 선언된 메소드를 호출하는 코드는 반드시 try-catch 블록으로 해당 예외를 잡아서 처리하거나, 혹은 자기 자신도 throws를 사용해 예외 처리의 책임을 또 다른 호출자에게 넘겨야 합니다. (마치 폭탄 돌리기 같죠? 💣)
- Unchecked Exception (RuntimeException 계열): NullPointerException, ArrayIndexOutOfBoundsException 등 RuntimeException과 그 자식 클래스들은 throws로 선언할 수는 있지만, 필수는 아니며 호출자에게 try-catch를 강제하지도 않습니다. (하지만 예상되는 경우 처리해주는 것이 좋습니다!)
- 예외 처리 강제: 사용자 정의 예외를 만들 때, 호출자가 반드시 처리하도록 강제하고 싶다면 Exception 클래스를 상속받으세요. 강제하고 싶지 않다면 RuntimeException을 상속받으면 됩니다.
throws를 사용하면, 각 메소드는 자신이 책임질 수 있는 예외만 처리하고, 그렇지 않은 예외는 호출한 쪽으로 전달하여 역할과 책임을 명확히 나눌 수 있습니다. 코드가 더 깔끔해지고 예외 처리 정책을 한 곳에서 관리하기 용이해지죠! 👍
3. 정리하며 📝
오늘은 예외 처리를 한 단계 더 업그레이드하는 방법을 배웠습니다! ✨
- try-with-resources: AutoCloseable 자원을 사용할 때 finally 블록 없이도 자동으로 자원을 해제해주어 코드를 간결하고 안전하게 만들어주는 마법! 🪄🤖
- throws: 메소드 내에서 발생 가능하지만 직접 처리하지 않을 Checked Exception을 선언하여, 호출한 쪽에 예외 처리의 책임을 명확히 전달하는 방법! 🙋♀️➡️🙋♂️
이제 여러분은 try-catch-finally의 기본기는 물론, try-with-resources로 자원 관리를 스마트하게 하고, throws로 예외 처리의 책임을 적절히 분담하는 방법까지 알게 되었습니다! 💪 이런 디테일들이 모여 더욱 견고하고 유지보수하기 좋은 코드를 만들 수 있게 되는 거랍니다! 꾸준히 연습해서 여러분의 것으로 만들어보세요! 💻

이야~ 오늘도 시간 가는 줄 모르고 열심히 달렸네요! 벌써 화요일 밤이라니! 🌃 예외 처리의 심화 내용까지 정복하시느라 정말 고생 많으셨습니다! 👏 이 지식들이 여러분의 코딩 실력 향상에 큰 도움이 되기를 진심으로 바랍니다! 😊
그리고 여러분! 그거 아세요?!! 벌써 내일이 수요일이에요!!! 🥳 수요일만 딱! 버티면 목요일 근로자의 날입니다!! 🎉 생각만 해도 신나지 않나요? 😄 조금만 더 힘내 보자구요!
오늘 하루도 정말 수고 많으셨습니다! 편안한 밤 보내시고, 우리는 다음 시간에 더 유익한 내용으로 다시 만나요! 👋 안녕~!
'데이터 엔지니어링' 카테고리의 다른 글
| 패스트캠퍼스 환급챌린지 31일차: 데이터엔지니어링 초격차 강의 후기 (1) | 2025.05.01 |
|---|---|
| 패스트캠퍼스 환급챌린지 30일차: 데이터엔지니어링 초격차 강의 후기 (0) | 2025.04.30 |
| 패스트캠퍼스 환급챌린지 28일차: 데이터엔지니어링 초격차 강의 후기 (0) | 2025.04.28 |
| 패스트캠퍼스 환급챌린지 27일차: 데이터엔지니어링 초격차 강의 후기 (0) | 2025.04.27 |
| 패스트캠퍼스 환급챌린지 26일차: 데이터엔지니어링 초격차 강의 후기 (1) | 2025.04.26 |