해당 포스트에서는 CheckedException 발생 시 다른 계층에서 이에 대한 의존관계를 억지로 참조해야하며, 코드의 가독성 및 기술 변경시에 대한 파급효과를 최소화 하고, 예외처리를 한곳에서 관리할 수 있는 다양한 이점이 있는데 이에 대한 내용을 담고 있다.
기본적으로 자바에서 제공하는 예외는 CheckedExcpetion, UncheckedException이 있다.
Thorwable : 최상위 예외
그 밑에 Excpetion, Error가 존재하는데, Error의 OutOfMemoryError는 메모리 부족이나 심각한 시스템 오류와 같은 애플리케이션에서 복구 불가능한 시스템 예외이다. 개발자는 해당 예외를 처리하거나 잡을 수 없다. 그리고 Throwable을 catch할 수 없다.
Throwable을 catch 할 경우 Error와 Excpetion 둘 다 잡게 되는데, 이 경우 애플리케이션 로직에서는 주로 비즈니스 로직과 관련된 예외를 처리하는 것이 일반적인데 Error까지 catch하게 된다면 비즈니스 로직이 아닌 시스템 레벨의 심각한 오류까지도 잡게 되니 의도치 않는 동작으로 이어질 가능성이 크다.
Exception : 체크예외 (SQLException, IOException)
애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외이다.
(상위에 Throwable, 하위에 Exception, Error -> Error는 시스템 레벨의 예외이기 때문에 잡아서 처리가 불가능한 예외이기 때문에)
Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외이다. (RuntimeException 제외)
Exception: RuntimeException
컴파일러가 체크하지 않는 언체크 예외이다(예외 처리가 강제적이지 않음. 컴파일러가 체크하지 않기 때문에 소스코드 밑에 빨간줄도 나타나지 않는다.).
자바에서의 예외 규칙
자바에서는 예외를 처리할 때 크게 2가지 선택지가 존재한다.
위에 그림을 보면 알 수 있듯이, 예외가 발생한 경우 어느 한 계층에서 예외를 잡아 처리하는 경우가 있고, 계속 자신을 호출한 곳으로 예외를 던지는 경우가 있다.
예외를 처리하지 못하고 계속 던질 경우, 자바 프로젝트라면 자바의 main()
쓰레드에서 예외 로그를 출력하면서 시스템이 종료된다.
혹은 WAS를 이용한 프로젝트일 경우, 시스템이 종료되진 않는다. WAS가 해당 예외를 받아서 기본적으로 설정한 예외 페이지를 보여주거나, 스프링 부트 프로젝트에서는 개발자가 지정한, 오류 페이지를 보여줄 수 있도록 되어있다.
강제로 예외처리를 해야하는 체크예외
CheckedException 같은 경우 현재 계층에서 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws
예외를 필수로 선언해야 한다. 그렇지 않으면 소스코드에 빨간줄이 그어지며 컴파일 오류가 발생하게 된다. 해당 부분 때문에 장점과 단점이 동시에 존재한다고 볼 수 있다.
장점으로는 개발자가 실수로 예외를 누락하지 않을 수 있다는 점.(컴파일러가 알려주기 때문에)
단점으로는 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 한다는 것이다. 게다가 해당 예외는 발생할 일이 없다는 것을 어떻게든 검증을 했더라도 CheckException은 강제이기 때문에 예외를throws
해줘야 한다.
게다가 강제이기 때문에 예외를 던졌을 경우 호출한 계층에서는 해당 예외에 대한 의존성 또한 강제로 부여받게 된다.
강제성이 없는 언체크 예외
UncheckedException은 예외를 잡아서 처리할 수 없을 때 예외를 밖으로 던지는 throws
를 생략할 수 있다.
이것 또한 장단점이 동시에 존재한다.
장점 중요치 않은 언체크 예외를 무시할 수 있으며, throws를 생략할 수 있다. 게다가 throws를 생략할 수 있기 때문에 다른 계층으로의 예외에 대한 의존관계 또한 참조하지 않아도 된다.
단점 개발자가 예외를 누락할 수있다는 점이다.
체크 예외의 단점 살펴보기
해당 그림을 살펴보자.
MVC 패턴으로 각 계층을 Spring MVC 프로젝트이다. 각각의 계층에는 명확히 자신이 해야 하는 일이 존재한다.
근데 여기서 문제점은 Repository에서 발생한 SQLException, NetworkClient에서 발생한 ConnectExcpetion이 있다.
해당 계층에서 자신의 계층에서 일어난 Excpetion을 throws할 경우 해당 Exception은 Servicer계층으로 가게 된다.
그럼 Service 계층은 비즈니스 로직을 담당하는 계층이지만 다른 계층으로 부터 메서드를 호출 할 경우 해당 계층에서 던지는 예외에 대한 의존관계를 참조해야 하는 경우가 발생한다. 그리고 이마저도 throws할 경우 이는 Controller로 가게 된다.
체크 예외 Throws시의 문제점
기술 변경시에 해당 문제에 대한 파급효과가 나타나게 된다는 것이다.
JDBC의 기술변경이 일어날 경우 이에 맞는 예외로 바꿔줘야 하며 Throws로 인해 예외를 전달받는 모든 계층에서 또한 변경이 일어나게 된다.
처리할 수 있는 체크 예외라면 서비스나 컨트롤러에서 처리하겠지만, 지금처럼 데이터베이스나 네트워크 통신처럼 시스템 레벨에서 올라온 예외들은 대부분 복구가 불가능하다.
Throws ExceptionSQLException
, ConnectException
같은 시스템 예외는 컨트롤러나 서비스에서는 대부분 복구가 불가능하고 처리할 수 없는 체크 예외이다. 따라서 자신들의 상위 계층인 Exception을 던저주면 되지 않을까 라고 생각할 수 있다.
결과적으로 체크 예외의 최상위 타입인 Excpetion
을 던지게 되면 하위 예외 타입이 모두 잡히기 때문에 다른 체크 예외를 잡을 수 있는 기능이 무효화 되어버린다. 중요한 예외를 놓치는 경우가 발생한다는 것이다.
따라서 어떤 타당한 이유가 있거나 특수한 상황이 아니라면 Exception 자체를 던지는 방법은 좋지 않은 방법이다.
언체크예외 활용하기(RuntimeException)
public class UncheckedAppTest {
static class RuntimeSQLException extends RuntimeException {
public RuntimeSQLException() {
}
public RuntimeSQLException(Throwable cause) {
super(cause);
}
}
static class RuntimeConnectException extends RuntimeException {
public RuntimeConnectException() {
}
public RuntimeConnectException(Throwable cause) {
super(cause);
}
}
static class Repository {
public void call() {
try {
runSQL();
} catch (SQLException e) {
throw new RuntimeSQLException(e);
}
}
private void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
static class NetworkClient {
public void call() {
throw new RuntimeConnectException("연결 실패");
}
}
static class Service {
Repository repository = new Repository();
NetworkClient networkClient = new NetworkClient();
public void logic() {
repository.call();
networkClient.call();
}
}
static class Controller {
Service service = new Service();
public void request() {
service.logic();
}
}
@Test
void unchecked() {
Controller controller = new Controller();
Assertions.assertThatThrownBy(controller::request)
.isInstanceOf(RuntimeException.class);
}
}
해당 코드에서 예외를 전환 해주었다.
- Repository에서 체크 예외인 SQLException이 발생하면 런타임 예외인
RuntimeSQLException
으로 전환 해서 예외를 던진다.- 이 때 기존 예외를 포함해주어야, 예외 출력 시 스택 트레이스에서 기존 예외도 함께 확인 할 수 있다.
런타임 예외 - 대부분 복구 불가능한 예외
시스템 레벨에서 발생한 예외는 복구 불가능한 예외이기 때문에 Service, Controller 계층에서 이를 신경 쓰지 않아도 된다. 게다가 의존관계 또한 참조하지 않을 수 있다.
런타임 예외를 사용하면 중간에 기술이 변경되어도 해당 예외를 사용하지 않는 컨트롤러, 서비스에서는 코드를 변경하지 않아도 된다.
런타임 예외 명시하기
런타임 예외는 위에서 말한 것처럼, 개발자가 누락할 수도있다는 것이며, 컴파일러가 이를 잡지 않는다. 그렇기 때문에 체크 예외를 언체크예외로 전환해서 사용한 경우에는 런타임 예외를 문서화 해야한다.
JPA EntityManager
/**
* Make an instance managed and persistent.
* @param entity entity instance
* @throws EntityExistsException if the entity already exists.
* @throws IllegalArgumentException if the instance is not an
* entity
* @throws TransactionRequiredException if there is no transaction when
- * invoked on a container-managed entity manager of that is of type
- * <code>PersistenceContextType.TRANSACTION</code>
*/
public void persist(Object entity);
JPA의 Javadoc을 보면 @throws 부분을 볼 수 있다. 이는 런타임 예외로 해당 에외들을 던진다는 것이다.
JdbcTemplate
/**
* Issue a single SQL execute, typically a DDL statement.
* @param sql static SQL to execute
* @throws DataAccessException if there is any problem
*/
void execute(String sql) throws DataAccessException;
런타임 예외도 throws
에 선언할 수 있다. 물론 생략해도 된다. 던지는 예외가 명확하고 중요하다면, 코드에 어떤 예외를 던지는지 명시되어 있기 때문에 개발자가 IDE를 통해서 예외를 확인하기가 편리하다.
물론 컨트롤러나 서비스에서 DataAccessException
을 사용하지 않는다면 런타임 예외이기 때문에 무시해도 된다.
정리
체크예외는 컴파일러가 잡아내기 때문에 강제적으로 예외처리를 해주어야한다. 체크 예외는 해당 라이브러리들이 제공하는모든 예외를 처리할 수 없을 때 마다 throws를 붙여주어야 한다.
체크 예외의 이런 문제점 때문에 최근 라이브러리들은 대부분 런타임 예외를 기본으로 제공한다.스프링 또한 대부분 런타임 예외를 제공한다.
'스프링' 카테고리의 다른 글
[Spring] 스프링의 @Transactional, 트랜잭션 기능의 추상화 (0) | 2024.07.13 |
---|---|
[Spring] 스프링이 DB 기술에 따라 예외를 제공하는 방법 (0) | 2024.07.13 |
[Spring] 서블릿의 예외 처리와 ExceptionHandler, ControllerAdvice 알아보기 (0) | 2024.05.17 |
[Spring] 서블릿의 Filter, 스프링의 Interceptor 예제 코드로 알아보기 (0) | 2024.05.16 |
[Spring] 로그인 기능 구현으로 알아보는 쿠키 및 세션 (2) | 2024.05.13 |