
Exception 중 RuntimeException은 언체크 예외로 throws, try-catch등으로 예외를 잡거나 던지지 않아도 된다. 이를 제외한 Exception클래스 예외들은 체크예외이므로 꼭 잡거나 던져줘야 한다. 그렇지 않으면 컴파일 시점에 오류가 발생한다.
throws : 예외를 잡을 수 없어서 상위로 던짐 try-catch : 예외를 잡음 throw : 예외를 터뜨림
예외를 반쪽자리를 잡는 것보다는, 정말 책임지고 처리할 수 있는 계층까지 던져서 그쪽에서 처리하도록 함을 의미한다.
catch(IOException e){
e.printStackTrace();
}
// 이렇게 예외를 잡는 것은, 단지 콘솔에 예외 메세지만 출력하고 끝임.
// 사용자 입장에서는 아무일도 안 일어난 것처럼 보이며, 로그도 콘솔에 찍힌 이후 사라짐(서버에선 콘솔 로그 못봄)
// 따라서 추후 문제 발생 시 정확한 원인 파악이 불가능함. 이렇게 애매하게 처리할 바엔 처리가능한 상위계층까지 던져야 함
//제대로 예외처리 하는 코드 예시
catch(FileNotFoundException e){
log.warn("파일이 존재하지 않습니다. 사용자에게 재업로드 요청");
model.attribute("message", "파일이 존재하지 않습니다. 재업로드 해주세요");
return "uploadForm"; //사용자에게 업로드 페이지 재출력
//사용자에게 알림 메시지 제공, 문제원인 설명, 재시도 가능한 UI 흐름 유지
즉, 예외는 정상 흐름을 벗어난 것이므로 회복 또는 중단을 명확히 해야한다.
예외 처리의 핵심은 복구 가능한 곳에서 처리하고, 그렇지 않으면 상위 계층에 책임을 위임하는 것이다.
log.info 같은 로그 라이브러리를 사용하는 이유는?콘솔출력은 왜 사용하면 안되나요...?
info,warn,error 등 로그 레벨 구분 불가)ELK,Grafana 등)에서 감지 자체가 되지 않는다.로그 라이브러리를 쓴다면?
error 레벨만 출력)즉, 애매하게 로그를 잡는 척 한다면 나중에 문제 발생 시 디버깅에 매우 어려움이 발생하므로,
확실하게 잡을 수 없다면 처리가 가능한 상위레벨로 던져야 한다!
-> 체크예외가 발생할 수 있는 상황이라면 예외처리를 강제해 컴파일 시점에 예외를 잡아줘야하고,
언체크예외가 발생할 수 있는 상황이라면 컴파일러 또한 예외여부를 모르므로 런타임 에러가 발생함.
어차피 잡을 수 없는 예외인데 체크예외로 선언한다면?
인터페이스, 레포지토리, 서비스 계층에서 모두 throws Exception을 선언해줘야 한다.
이렇게 되면 해당 코드 때문에 의존관계가 생겨 기술 변경 시 코드 변경이 필요하다.
런타임예외는 보통 잡지 않고, throws도 하지 않기 때문에 최상위계층에서 걸린다. 이렇게 되면 한 곳에서 일괄 처리가 가능하게 된다.
복구가 가능하다! -> 사용자에게 재시도를 요청하는 등 복구가 가능한 경우는 보통 체크예외 사용
복구가 불가능하다! -> 프로그래머 실수나 논리적 오류 등 복구가 불가한 경우는 언체크예외 사용
런타임 예외보다 컴파일 예외가 좋다고 해서 모든 예외를 체크예외로 만들게 되면 코드가 try-catch 지옥이 될 수 있다. 그렇다고 모든 코드를 런타임 예외로 두는 것 또한 좋지 않다.
예를 들어 정말 중요한 처리가 필요한 로직이라면, (계좌 이체 실패 등) 런타임예외로 둘 경우 중요한 예외(예: 계좌 이체 실패, 결제 오류 등)도 개발자가 실수로 무시할 수 있음 → 신뢰성↓, 위험↑
예시: “계좌 이체 실패”는 체크 예외? 언체크 예외?
복구 가능한 실패 (예: 사용자가 잘못된 계좌번호 입력 → 다시 입력 요청) → 체크 예외
논리적 오류나 시스템 장애 (예: 내부 버그로 인해 잔고 부족인데 이체 시도됨) → 언체크 예외
예외는 책임과 의도를 분리하는 도구로서 이해해야 한다.
만약, DB예외를 모두 SQLException으로 던지던 중, 몇몇 예외는 내가 직접 잡아서 처리하고 싶을 때(ex. 아이디 중복 오류 등)는 새로운 사용자정의 예외를 만들거나 스프링의 추상화된 예외클래스를 사용하게 된다.
이때 필수적으로 이전 예외의 cause 인자를 파라미터로 전달해야 디버깅원인(루트cause)을 명확하게 파악할 수 있다.
catch(SQLException e){
throw new RuntimeSQLException(e)
// 인자로 전달하지 않으면 새로 변경된 예외 이후부터의 trace만 확인가능
}
결론
예외 처리의 핵심은 다음과 같다
✅ 책임지고 처리할 수 있는 계층에서 예외를 처리하라
✅ 복구 가능한 상황 = 체크 예외, 복구 불가능한 상황 = 언체크 예외
✅ 예외는 숨기지 말고, 명확하게 처리하거나 명확하게 던져라
✅ 로그는 꼭 Logger를 사용하고, 콘솔출력은 피하라
✅ 예외 변환 시 cause 반드시 포함하라