[Spring] 체크 예외(Checked Exception) vs 언체크 예외(Unchecked Exception)/런타임 예외(RuntimeException), 예외전환

에러(Error)와 예외(Exception)

 

에러와 예외
[그림 1] Error와 Exception

 

체크 예외와 언체크 예외에 알아보기 전에 Error와 Exception에 대해 알아보자.

 

 

자바에서 오류(Error)란 시스템이 종료되어야 할 수준의 수습하기 힘든 심각한 문제를 의미한다. 이러한 오류는 개발자가 미리 예측하여 처리하기 힘든 것이 대부분이기 때문에 오류에 대한 처리는 매우 힘들다.

 

반면 예외(Exception)는 개발자가 구현한 로직에서 발생한 실수 혹은 사용자의 영향에 의해 발생하는 예측하고 방지할 수 있는 문제를 뜻한다. 

 

 

참고) 

 

 

[JAVA] 예외 처리(Exception Handling)에 대하여, try-catch문, 오류(error)와 예외(exception)

오류(error)와 예외(exception) 자바에서 오류(error)란 시스템이 종료되어야 할 수준의 수습하기 힘든 심각한 문제를 의미한다. 이러한 오류는 개발자가 미리 예측하여 처리하기 힘든 것이 대부분이

code-lab1.tistory.com

 

 

 

 

체크예외(Checked Exception)

체크 예외는 컴파일러가 체크하는 예외이다. Exception을 상속받은 예외는 모두 체크 예외가 된다. 다만 RuntimeException을 상속받는 예외는 예외로 한다.

 

이러한 체크 예외는 잡아서(catch) 처리하거나 혹은 밖으로 던지도록(throw) 선언해야만 컴파일 오류가 발생하지 않는다.

 

Unhandled Exception
[그림 2] Unhandled exception 컴파일 에러

[그림 2]는 IOException을 발생시키는 코드이다. IOException이나 SQLException은 대표적인 체크 예외인데, 위와 같이  체크 예외를 처리하지 않으면 컴파일 오류를 발생시킨다.

 

 

throw IOEXception
[그림 3] 예외 처리

 

이때 [그림 3]처럼 IOException을 던져(throw)주면 컴파일 에러가 사라진다.

 

 

이처럼 체크 예외는 컴파일러가 예외를 체크하기 때문에 개발자가 실수로 예외를 누락하지 않도록 잡아준다. 하지만 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 크게 신경 쓰고 싶지 않은 예외까지 모두 챙겨야 한다. 

 

 

체크 예외의 문제점 

체크 예외의 문제점
[그림 4] 체크 예외의 문제점 (출처 : 참고1)

 

상황: 

 

1. Repository 에서는 DB에 접근해 데이터를 저장하고 관리한다. 여기서는 SQLException 체크 예외를 던진다.

2. NetworkClient 는 외부 네트워크에 접속해 어떤 기능을 처리한다. 여기서는 ConnectException 체크 예외를 던진다.

3. Service는 Repository와 NetworkClient를

4. 따라서 Service는 SQLException과 ConnectException을 처리해야 한다. 하지만 SQLException이나 ConnectException 같은 문제들은 대부분 애플리케이션 로직에서 처리하기 힘들다.

5. Service는 SQLException과 ConnectException을 처리할 수 없으므로 둘 다 밖으로 던진다.

6. Controller도 두 예외를 처리할 방법이 없어 예외를 밖으로 던진다.

7. 서블릿의 오류 페이지나 혹은 스프링 MVC가 제공하는 ControllerAdvice에서 이런 예외를 공통으로 처리한다.

 

 

class Controller {
    public void request() throws SQLException, ConnectException {
        service.logic();
    }
}

class Service {
    public void logic() throws SQLException, ConnectException {
        repository.call();
        networkClient.call();
    }
}

 

예를 들어 위와 같이 throws SQLException, ConnectException 처럼 예외를 던지는 부분이 코드에 선언되어 있다고 하자.

 

이때 Service와 Controller는 java.sql.SQLException 등에 의존하게 되므로 문제가 발생한다.

 

만약 기존 Repository가 JDBC 기술을 사용하고 있다가 나중에 JPA를 사용하여 SQLException이 아닌 JPAException으로 예외가 변경된다면 어떨까?

 

체크 예외의 의존성 문제
[그림 5] 체크 예외의 의존성 문제 (출처 : 참고1)

 

SQLException을 던지는 모든 부분을 JPAException을 던지도록 수정해야 할 것이다. 

 

즉, 체크 예외를 사용하면 하위에서 올라온 복구 불가능한 예외를 상위 클래스가 모두 알고 있어야 하는 불필요한 의존관계 문제가 발생하게 된다.

 

이때 이러한 예외를 모두 처리할 수 있는 Exception을 던지는 식으로 문제를 해결하면 어떨까?

 

void method() throws Exception {..}

 

이렇게 하면 SQLException이든, JPAException 이든 모두 Exception에 포함되어 있으니 의존관계를 신경 쓰지 않아도 되지 않을까? 물론 그렇지만, 이렇게 하면 모든 예외를 다 던지기 때문에 체크 예외를 의도한 대로 사용하는 것이 아니다.

어떤 예외를 잡고 어떤 예외를 던지는지 알 수 없기 때문이다.

 

 

이러한 문제들로 인해 실제 개발에서는 대부분 언체크 예외를 사용한다.

 

 

언체크 예외(Unchecked Exception)/런타임 예외(RuntimeException)

언체크 예외는 말 그대로 컴파일러가 예외를 체크하지 않는다. 언체크 예외는 기본적으로 체크 예외와 동일하지만, 예외를 던지는 throws 를 선언하지 않고 생략 가능하다. 이 경우 자동으로 예외를 던진다.

 

모든 런타임예외(RuntimeException)은 언체크 예외이다.

 

언체크 예외
[그림 6] 언체크 예외

 

[그림 6]은 언체크 예외인 NullPointerException을 발생하는 코드이다. throw를 통해 예외를 던지는 코드를 명시하지 않아도 오류가 발생하지 않는다.

 

언체크 예외는 신경 쓰고 싶지 않은 언체크 예외를 무시할 수 있다. 따라서 신경 쓰고 싶지 않은 예외의 의존관계를 참조하지 않아도 된다.  

 

또한, 스프링 프레임워크가 제공하는 선언적 트랜잭션(@Transactional)안에서 에러 발생 시 체크 예외는 롤백이 되지 않고, 언체크 예외는 롤백이 된다.

 

하지만 언체크 예외는 개발자가 실수로 예외를 누락할 수 있다는 단점도 존재한다.

 

예외 전환

체크 예외인 SQLException을 런타임 예외인 RuntimeSQLException으로 전환해보자.

 

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 RuntimeSQLException extends RuntimeException {
    public RuntimeSQLException(Throwable cause) {
        super(cause);
    }
}

 

런타임 예외를 상속받는 RuntimeSQLException 클래스를 만들고 SQLException이 발생하면 RuntimeSQLException을

시에서 기존 예외도 함께 확인할 수 있다.

 

이렇게 체크 예외를 런타임 예외로 전환하면, Repository를 쓰는 상위 클래스 (Service, Controller) 등에서는 의존관계에 대해서 신경 쓰지 않아도 된다.

 

 

런타임 예외 문서화

런타임 예외는 개발자가 예외를 누락할 수도 있기 때문에 문서화를 잘해야 한다.

 

/**
* 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 EntityManager 에는 위와 같은 persist 메서드가 있는데, 어떤 예외들을 던질 수 있는지 주석(문서)으로 남겨놓았다.

 

/**
* 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를 선언할 수도 있다. 

 

 

 


참고

 

1. 인프런 김영한 강의 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1

2. https://mangkyu.tistory.com/152

 

반응형

댓글

Designed by JB FACTORY