예외도 객체이므로 Ojbect
를 상속하는 Throwable
클래스부터 시작한다.
Throwable
하위로는 Exception
과 Error
클래스가 있다.
Error
클래스는 어플리케이션 레벨에서 다룰 수 없는 시스템 장애를 말한다.
예) OutOfMemoryError
Exception
클래스를 포함한 하위 클래스들은 어플리케이션 레벨에서 다룰 수 있는 예외들이다.
Exception
클래스의 하위 클래스는 전부 체크 에외(Checked Exception)이지만 RuntimeException
과 그 하위 클래스들은 제외된다.
예외에는 기본적으로 두 가지 규칙이 있다.
- 예외는 잡아서 처리하거나 던져야 한다.
- 예외를 잡거나 던질 때 지정한 예외 뿐만 아니라 그 예외의 하위 예외들도 함께 처리된다.
컴파일러가 체크하는 예외이다.
RuntimeException
과 그 하위 예외를 제외한 나머지 모든 예외들이 해당된다.
체크 예외는 잡아서 처리하거나 외부로 던지도록 선언해야 하며, 이렇게 처리되지 않은 예외는 컴파일 오류가 발생한다.
RuntimeExcpetion
과 그 하위 예외들이며, 컴파일러가 예외를 체크하지 않는다는 점에서 언체크 예외라고 한다. throws로 예외를 선언하지 않고 생략할 수 있다. 이 경우 자동으로 예외를 던지며, 명시적으로 선언할 수도 있다.
- 개발자가 다룰 수 없는 예외까지 모든 예외를 처리하는 것은 무척 번거롭다.
- 예외를 던지게 될 경우 불필요한 의존 관계가 생긴다.
불필요한 의존 관계에 대한 하나의 예로 JDBC 기술을 사용할 때 발생하는 SQLException
을 들 수 있다. SQLException
은 체크 예외이다. 체크 예외는 컴파일 오류가 발생하므로 반드시 잡아서 처리하거나 상위 레벨로 던져야 한다.
문제는 이 예외가 SQL 문법 오류 등으로 발생하는 것이기 때문에 시스템 상에서 마땅히 처리할 수 있는 예외가 아니라는 점이다.
결국 이 예외를 처리할 수 있는 곳이 없어 상위 레벨로 계속 던져지다 보면 최상위 레벨인 컨트롤러까지 전달되게 되고, 컨트롤러는 메서드 선언부에 throws SQLException
를 선언하게 된다.
이 상황은 문제가 있다. 컨트롤러는 표현 계층임에도 불구하고 레포지토리 레벨의 특정 기술(JDBC를 사용함으로써 발생하는 예외)에 의존하게 되는 셈이다.
따라서 OCP, DI 원칙을 위배하므로, 구체적인 기술이 변경되었을 때 표현 영역까지 영향도가 전파되는 문제를 갖는다.
위에서 말한 두 가지 문제점을 해결하기 위해 비즈니스 예외들은 런타임 예외로 구현해주는 것이 좋다. 다만 이 경우 예외 처리가 누락될 수 있으니 해당 예외들을 빠뜨리지 않고, 다룰 수 있도록 문서화 등을 통해 공유하는 것이 중요하다.
물론 꼭 다루어져야 하는 치명적인 예외라면 체크 예외로 구현하는 것도 좋은 방법이 될 수 있다.
이전 예외를 파라미터로 전달해주어야 한다. Throwable cause 라는 파라미터로 예외를 전달해주어야 로그 출력 시 Caused By 문구를 통해 해당 예외가 어떤 예외를 이어 받았는지 추적할 수 있다.