컴파일러가 예외 누락을 체크해주기 때문에, Exception을 잡아서 try-catch로 처리하거나 혹은 throws를 선언해야한다.
말 그대로 Runtime(실행 중)단계에 발생할 수 있는 에외로, Exception을 잡아서 처리하지 않아도 throws를 생략할 수 있다.
Unchecked Exception(RuntimeException)
을 사용한다.Checked Exception
을 사용한다.Checked Excepton
은 컴파일러가 예외 누락을 체크해주기 때문에, 개발자가 실수로 예외를 놓치는 것을 방지한다. 따라서 try-catch 혹은 throws를 명시적으로 작성해주어야 한다.
하지만 이러한 방식이 좋아보일지는 몰라도, 치명적인 문제점이 발생한다.
위 그림은 Repository와 NetworkClient 계층에서 Checked Exception
이 발생했을 때의 도식도이다. (SQLException, ConnectException은 Checked Exception이다.)
Respository와 NetworkClient에서 throws
한 SQLException, ConnectException
은 Service, Controller 계층에서도 모두 throws
를 선언해야 한다.
Spring MVC에서는 Controller에서 throws
한 것을 ControllerAdvice에서 예외를 공통으로 처리한다.
SQLException, ConeectException
과 같이 복구가 불가능한 예외는 별도의 오류 로그를 남겨야 하며, 개발자가 오류를 인지할 수 있도록 최대한 빨리 알려야 한다.
예를 들어, SQLException
이 잘못된 SQL을 작성해서 발생했다면, 해당 SQL을 수정해서 다시 배포하기 전까지 복구가 불가능하며, 사용자는 같은 문제를 겪게 된다.
class Repository {
public void call() throws SQLException {
throw new SQLException("ex");
}
}
class NetworkClient {
public void call() throws ConnectException {
throw new ConnectException("연결 실패");
}
}
class Service {
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic() throws SQLException, ConnectException {
repository.call();
networkClient.call();
}
}
서비스는 이 둘을 모두 호출하게 된다.
하지만, 서비스에서 처리가 불가능하기 때문에 throws
한다.
class Controller {
Service service = new Service();
public void request() throws SQLException, ConnectException {
service.logic();
}
}
결국 SQLException과 ConnectException은 어디에서도 처리가 불가능하며, 2가지 문제가 발생한다.
SQLException, ConnectException
을 의존하게 된다.코드로 알아보자.
class RuntimeSQLException extends RuntimeException {
public RuntimeSQLException(Throwable cause) {
super(cause);
}
}
이때 생성자를 Throwable를 받게 되면 기존에 발생한 예외를 넣을 수 있다.
class RuntimeConnectException extends RuntimeException {
public RuntimeConnectException(String message) {
super(message);
}
}
class Repository {
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e);
}
}
private void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
Repository에서 SQLException이 발생하도록 하여 잡아서 RuntimeSQLException을 호출하도록 하여 체크 예외를 처리한다.
위 코드와 같이 기존의 예외를 포함하여 처리해야 스택 트레이스에서 확인이 가능하다.
class NetworkClient {
public void call() {
throw new RuntimeConnectException("연결 실패");
}
}
class Service {
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic() {
repository.call();
networkClient.call();
}
}
Unchecked Exception
이기 때문에 throws를 선언하지 않아도 된다.
class Controller {
Service service = new Service();
public void request() {
service.logic();
}
}
마찬가지로 Unchecked Exception
이기 때문에 throws를 선언하지 않아도 된다.
@Slf4j
public class UncheckedAppTest {
@Test
void unchecked() {
Controller controller = new Controller();
assertThatThrownBy(() -> controller.request())
.isInstanceOf(Exception.class);
}
@Test
void printEx() {
Controller controller = new Controller();
try {
controller.request();
} catch (Exception e) {
// e.printStackTrace();
log.info("ex", e);
}
}
}
throws를 선언하지 않아도 정상적으로 예외가 발생하는 것을 검증하는 코드로 정상 종료된다.
Unchecked Exception
을 사용함으로,
Checked Exception
처럼 예외를 강제로 의존하지 않아도 된다.