메모리 부족이나 심각한 시스템 오류와 같이, 애플리케이션에서 복구 불가능한 시스템 예외이므로 애플리케이션 개발자는 이 예외를 잡으면 안된다.
컴파일러가 체크하는 체크 예외이며, Exception
의 자식 클래스들까지 모두 해당한다. (Runtime 제외) 따라서, 예외를 throws
로 넘기거나 try - catch
로 잡아서 반드시 처리해주어야 한다.
컴파일러가 체크하지 않는 언체크 예외이며, RuntimeException
의 자식 클래스들까지 모두 해당한다. 참고로 Error
도 언체크 예외이다.
🔖 예외 기본 규칙
1. 예외를 잡아서 처리한다면, 정상 흐름으로 전환할 수 있다.
2. 예외를 상위 계층으로 던진다면, 자바main()
스레드의 경우 예외 로그를 출력하면서 시스템이 종료된다.
따라서 서비스가 죽으면 안되는 웹 애플리케이션에서는 WAS가 예외를 받아서 처리해준다.
3. 예외를 잡거나 던질때는 해당 예외의 자식들까지 함께 처리된다.
체크 예외는 잡거나 밖으로 던지도록 선언하지 않으면, 컴파일 에러가 발생한다.
public void call() throws MyCheckedException { // 메서드 선언 필수
throw new MyCheckedException("ex");
}
개발자가 실수로 예외를 누락하지 않도록, 컴파일 단계에서 체크해주는 안전 장치이다.
실제로 개발자가 모든 체크 예외를 잡거나 던져야 하기 때문에, 너무 번거롭게 된다. 해당 계층에서 잡아서 처리하지 못하는 예외임에도 신경 쓰고 throws
처리를 해줘야 하기 때문이다.
언체크 예외는 말그대로 컴파일러가 체크하지 않는 예외이다.
즉, 체크 예외와 달리 throws
를 생략해도 자동으로 예외를 던져주는 것이다. 헷갈리면 안되는 것이 잡거나 던지거나 둘 중 하나를 해야하는 것은 체크 예외와 언체크 예외 모두 동일하다.
public void call() {
throw new MyUncheckedException("ex");
}
위와 같이 선언부에 throws
를 생략해도 컴파일 에러가 나지 않는다.
필요한 경우, 예외를 잡아서 처리하고 정상 흐름으로 변환한다.
public void callCatch() {
try {
repository.call();
} catch (MyUncheckedException e) {
log.info("예외 처리, message={}", e.getMessage());
}
}
예외를 잡지 않아도 자연스럽게 상위로 넘어간다.
public void callThrow() { // throws MyUncheckedException (X)
repository.call();
}
🔖 일반적인 사용법
1. 기본적으로는 런타임 예외를 사용한다.
2. 중요하고 의도적인 비즈니스적 예외(ex) 계좌 이체 실패, 잔액 부족, 로그인 비밀번호 불일치)일때는 선택적으로 체크 예외를 사용한다.
얼핏 보면 강제성이 있는 체크 예외가 런타임 예외보다 안전해보이지만, 기본적으로 런타임을 사용해야 하는 이유는 무엇일까? 바로 체크 예외의 아래와 같이 치명적인 단점이 2가지 있기 때문이다.
일반적으로 대부분의 예외는 복구 불가능하다. 데이터베이스나 네트워크 통신과 같은 시스템 레벨 쪽에서 올라온 예외들은 애플리케이션 로직 단에서 코드로 복구할 수 있는 방법이 없기 때문이다.
하지만 체크 예외를 사용한다면 복구할 수 없음에도 서비스와 컨트롤러는 밑에 계층에서 발생하는 예외를 throws
로 처리를 해줘야 하는 문제가 존재한다.
복구 불가능한 예외들은 로그만 남기고 웹 애플리케이션이라면 서블릿의 오류 페이지나, 또는 스프링 MVC가 제공하는
ControllerAdvice
에서 공통으로 처리해야 해서 사용자에게 알려야 한다.
class Controller {
public void request() throws SQLException, ConnectException {
service.logic();
}
}
class Service {
public void logic() throws SQLException, ConnectException {
repository.call();
networkClient.call();
}
}
SQLException
이 서비스 계층 코드에 들어있다는 것은 JDBC
와 같은 특정 기술에 의존하게 되는 것과 마찬가지이므로, 불필요한 의존관계가 발생해 유지보수가 힘들어진다. (구체적인 기술에 대한 예외에 의존)
🔖 throws Exception 을 하면 안되는 이유
void method() throws Exception {..}
Exception
은 최상위 타입이므로, 중요한 체크 예외까지 모두 밖으로 던져서 확인할 수 없게 되는 문제가 발생한다.
런타임 예외이기 때문에 서비스, 컨트롤러는 해당 예외들을 처리할 수 없다면 별도의 선언 없이 그냥 두면 된다.
static class Repository {
public void call() {
try {
runSQL();
} catch (SQLException ex) { // 복구 불가능한 예외
throw new RuntimeSQLException(ex); // 런타임 예외로 바꾸기(이전 예외를 포함)
}
}
...
}
레파지토리에서 체크 예외인 SQLException
이 발생하면 런타임 예외인 RuntimeSQLException
으로 전환해서 다시 던지고 있는 것을 볼 수 있다. 이때, 반드시 기존 예외를 포함해주어야 예외 출력시 스택 트레이스에서 기존 예외도 함께 확인할 수 있다.
🔖 예외 포함과 스택 트레이스
예외를 전환할때는, 꼭 기존 예외를 포함해서 던져야 한다.
문제 해결의 실마리가 되는 루트 예외에 들어있는 근본 원인 정보들이 날라가는 것을 방지하지 때문이다.
참고로 런타임 예외는 놓칠 수 있기 때문에, 문서화를 잘 하거나 throws
를 통해 명시적으로 알려주면 된다.
대부분 자바나 스프링 라이브러리들은 런타임을 사용한다. 그리고 예외를 공통으로 처리하는 부분을 앞단에서 만들어서 처리하면 된다.