개발자에게 있어 **예외 처리(Exception Handling)**는 프로그램의 안정성과 신뢰성을 보장하는 핵심 요소입니다. 아무리 완벽하게 코드를 작성해도 예기치 않은 상황(파일 없음, 네트워크 오류, 0으로 나누기 등)은 발생할 수 있습니다. 이때 try-catch-finally 구문을 어떻게 사용하느냐에 따라 코드가 유지보수하기 쉬운 '깔끔한 코드'가 될 수도 있고, 디버깅을 어렵게 만드는 '스파게티 코드'가 될 수도 있죠.
이 글에서는 try-catch-finally 구문을 활용하여 예외 처리를 깔끔하고 효과적으로 하는 실전 노하우를 공유합니다.
1. try-catch-finally 기본 이해
키워드 | 역할 | 실행 시점 |
try | 예외 발생 가능성이 있는 코드를 감싸는 블록. | 실행 시도. |
catch | try 블록에서 발생한 특정 예외를 잡아 처리하는 블록. | try에서 예외 발생 시 해당 예외 타입과 일치할 경우 실행. |
finally | 예외 발생 여부와 관계없이 항상 실행이 보장되는 블록. | try 또는 catch 블록 실행 후 반드시 실행. (단, JVM 종료와 같은 특수 상황 제외) |
💡 실전 노하우: finally는 리소스 정리 전용!
finally 블록의 가장 중요한 용도는 **리소스 정리(Resource Cleanup)**입니다. 파일 스트림, 데이터베이스 연결, 네트워크 소켓 등 사용 후 반드시 닫아줘야 하는 리소스를 해제하는 코드를 finally에 넣으세요.
Connection conn = null;
try {
conn = DB_Connect.getConnection();
// 데이터베이스 작업...
} catch (SQLException e) {
// 예외 처리 로직...
} finally {
// 예외 발생 여부와 관계없이 연결을 반드시 닫아줘야 함
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
// close 중 발생하는 예외는 별도로 처리
}
}
}
📌 최신 언어의 발전: Java의 **try-with-resources**나 C#의 using 구문처럼, 많은 언어에서 리소스 해제를 자동으로 처리하는 기능을 제공합니다. 가능하다면 이 기능을 사용해 finally 블록 자체를 없애고 코드를 더 깔끔하게 만들 수 있습니다.
2. 깔끔한 catch 처리를 위한 핵심 원칙
2.1. 구체적인 예외 타입을 명시하세요 (Specific Catch)
모든 예외를 잡아내는 **catch (Exception e)**와 같은 포괄적인 처리는 지양해야 합니다. 이는 마치 모든 질병에 똑같은 약을 처방하는 것과 같습니다.
- 나쁜 예: catch (Exception e) { log.error("에러 발생!"); }
- 좋은 예: catch (FileNotFoundException e) { ... }, catch (IOException e) { ... }
📝 다중 catch의 순서: 여러 개의 catch 블록을 사용할 때는 **하위 클래스(구체적인 예외)**를 먼저 작성하고, **상위 클래스(포괄적인 예외)**를 나중에 작성해야 합니다. (예: FileNotFoundException을 먼저, IOException을 나중에)
2.2. 예외를 먹어버리지 마세요 (Don't Swallow Exceptions)
catch 블록 내에서 아무 작업도 하지 않고 예외를 무시(Swallow)하는 것은 최악의 관행입니다.
try {
// 중요한 작업
} catch (Exception e) {
// 이 안에 아무것도 없다면?
// 예외가 발생했지만 아무도 모르게 조용히 사라짐 😱
}
예외를 처리하기로 결정했다면, 다음 세 가지 중 최소한 하나는 수행해야 합니다.
- 로그 기록 (Logging) 🪵: log.error("파일 로드 실패", e);와 같이 예외 객체(e) 자체를 포함하여 로그에 기록하세요. 스택 트레이스(Stack Trace)를 남겨야 문제의 근원지를 추적할 수 있습니다.
- 복구 로직 실행 (Recovery): 사용자에게 재시도 옵션을 제공하거나, 기본값을 설정하여 프로그램을 정상적으로 진행합니다.
- 예외 재발생/전파 (Rethrowing/Chaining): 현재 계층에서 처리할 수 없는 예외라면, 적절한 추상화 레벨의 새로운 예외로 감싸서(throw new CustomException("메시지", e);) 호출자에게 던져(전파)줍니다.
3. 예외를 던지는(Throwing) 노하우
3.1. 책임을 분리하세요 (Separation of Concerns)
예외 처리는 발생 지점과 처리 지점의 책임을 분리하는 것이 중요합니다.
- 발생 지점 (하위 레이어): 데이터 접근(DAO)이나 비즈니스 로직은 상세한 기술적 예외(e.g., SQLException, IOException)를 발생시킵니다.
- 처리 지점 (상위 레이어): 컨트롤러나 서비스 계층에서 이 기술적 예외를 잡아 비즈니스적 의미를 담은 **사용자 정의 예외(Custom Exception)**로 변환하거나, 사용자에게 보여줄 친절한 메시지로 처리합니다.
예시: DAO에서 SQLException 발생 → Service에서 잡아 UserNotFoundException으로 변환 → Controller에서 사용자에게 "사용자를 찾을 수 없습니다." 메시지 응답.
3.2. throw new RuntimeException(...)은 신중하게
RuntimeException(언체크 예외)은 컴파일러가 강제하지 않으므로 코드를 간결하게 만들 수 있습니다. 그러나 이를 남발하면 예외 처리가 필요 없는 코드처럼 보여 심각한 오류가 숨겨질 수 있습니다.
- 적절한 사용: 개발자가 막을 수 없는 오류(NullPointer, IndexOutOfBounds 등)나, 복구할 수 없는 비즈니스 로직 오류에 사용.
- 지양할 사용: 복구 가능하거나 반드시 호출자에게 처리를 강제해야 하는 상황(IO, File, DB 접근 등)에는 Exception(체크 예외)을 사용하는 것이 더 안전합니다.
✨ 결론: 좋은 예외 처리는 '친절함'이다.
깔끔한 예외 처리는 결국 두 가지에 대한 '친절함'으로 귀결됩니다.
- 사용자에게 친절: 발생한 문제에 대해 명확하고 이해하기 쉬운 피드백을 제공하여 불필요한 혼란을 줄여줍니다.
- 미래의 개발자(혹은 나 자신)에게 친절: 로그에 충분한 스택 트레이스를 남겨 문제의 근본 원인을 쉽게 파악하고, 구체적인 예외 타입을 사용하여 코드를 읽는 것만으로 예외 시나리오를 예측할 수 있게 합니다.
지금 바로 여러분의 코드에서 무심히 지나쳤던 catch (Exception e) {} 블록을 점검하고, 더 깔끔하고 명확하게 개선해보세요! 🚀
'백엔드 > Java' 카테고리의 다른 글
🔥 자바 8 이후 필수 문법: 람다(Lambda)와 스트림(Stream) API 활용법 (0) | 2025.10.08 |
---|---|
자바 컬렉션 프레임워크 뽀개기: List, Set, Map 언제 사용해야 할까? (0) | 2025.10.06 |
Spring Boot와 Java: 백엔드 개발의 기본기 (0) | 2025.10.05 |
[개인 프로젝트] 게시판 만들기 6 - Comment Dao, Service (0) | 2025.10.03 |
왜 자바는 여전히 강력한가? (0) | 2025.10.01 |