[스프링]Checked Exception vs Unchecked Exception 정리

JANG SEONG SU·2023년 10월 15일
0

Sping

목록 보기
6/9

Checked vs Unchecked Exception(에외)

Checked Exception

컴파일러가 예외 누락을 체크해주기 때문에, Exception을 잡아서 try-catch로 처리하거나 혹은 throws를 선언해야한다.

Unchecked Exception

말 그대로 Runtime(실행 중)단계에 발생할 수 있는 에외로, Exception을 잡아서 처리하지 않아도 throws를 생략할 수 있다.

Exception 기본 원칙

  • 기본적으로는 Unchecked Exception(RuntimeException)을 사용한다.
  • 비즈니스 로직상 의도적으로 던지는 예외에만 Checked Exception을 사용한다.
    • 예)계좌 이체 실패 예외, 결제시 포인트 부족 예외, 로그인 ID/PW 불일치 예외 등 예외를 반드시 잡아서 처리해야하는 중요한 문제일 때 사용

Checked Exception의 한계

Checked Excepton은 컴파일러가 예외 누락을 체크해주기 때문에, 개발자가 실수로 예외를 놓치는 것을 방지한다. 따라서 try-catch 혹은 throws를 명시적으로 작성해주어야 한다.

하지만 이러한 방식이 좋아보일지는 몰라도, 치명적인 문제점이 발생한다.

위 그림은 Repository와 NetworkClient 계층에서 Checked Exception이 발생했을 때의 도식도이다. (SQLException, ConnectException은 Checked Exception이다.)

Respository와 NetworkClient에서 throwsSQLException, ConnectException은 Service, Controller 계층에서도 모두 throws를 선언해야 한다.

Spring MVC에서는 Controller에서 throws한 것을 ControllerAdvice에서 예외를 공통으로 처리한다.

  • 이 경우 보통 HTTP 상태 코드 500(내부 서버 오류)을 응답하게 된다.
  • 사용자에게 어떤 문제가 발생했는지 자세히 설명하기 어렵다.
  • 사용자에게 문제 발생을 알리더라도 보안에 문제가 생길 수 있다.

SQLException, ConeectException과 같이 복구가 불가능한 예외는 별도의 오류 로그를 남겨야 하며, 개발자가 오류를 인지할 수 있도록 최대한 빨리 알려야 한다.

예를 들어, SQLException이 잘못된 SQL을 작성해서 발생했다면, 해당 SQL을 수정해서 다시 배포하기 전까지 복구가 불가능하며, 사용자는 같은 문제를 겪게 된다.

코드로 알아보는 Checked Exception

Repository

class Repository {
    public void call() throws SQLException {
        throw new SQLException("ex");
    }
}

NetworkClient

class NetworkClient {
    public void call() throws ConnectException {
        throw new ConnectException("연결 실패");
    }
}

Service

class Service {
    Repository repository = new Repository();
    NetworkClient networkClient = new NetworkClient();

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

서비스는 이 둘을 모두 호출하게 된다.
하지만, 서비스에서 처리가 불가능하기 때문에 throws한다.

Controller

class Controller {
    Service service = new Service();

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

문제점

결국 SQLException과 ConnectException은 어디에서도 처리가 불가능하며, 2가지 문제가 발생한다.

  • 복구 불가능한 예외
    • SQLException이 발생했다고 가정하면 SQL 문법 오류, 데이터베이스의 문제, 서버가 중간에 종료되는 등의 문제가 발생했을 수도 있다.
      이러한 문제들은 대부분 복구가 불가능하며 Service나 Controller에서 문제를 해결할 수 없는 경우가 대부분이다.
      즉, 로그를 통해 개발자가 오류를 빠르게 인지하는 것이 중요하며, 서블릿 필터, 스프링 인터셉터, ControllerAdvice를 사용하여 해결해야 한다.
  • 의존 관계에 대한 문제
    • Service와 Controller 계층에서 SQLException, ConnectException을 의존하게 된다.
      만약, JDBC를 사용하다가 JPA와 같은 기술로 변경하게 된다면, SQLException에 의존하던 모든 서비스와 컨트롤러의 코드를 JPAException에 의존하도록 바꿔야 한다.
      즉, DI를 통해 코드 변경 없이 구현체를 변경할 수 있는 장점이 훼손된다.

Unchecked Exception 활용

`RuntimeException`을 상속하는 `RuntimeSQLException, RuntimeConnectException`를 대신 발생하게 하면 Checked Exception의 문제를 해결할 수 있다.

코드로 알아보자.

RuntimeSQLException

class RuntimeSQLException extends RuntimeException {
    public RuntimeSQLException(Throwable cause) {
        super(cause);
    }
}

이때 생성자를 Throwable를 받게 되면 기존에 발생한 예외를 넣을 수 있다.

RuntimeConnectException

class RuntimeConnectException extends RuntimeException {
    public RuntimeConnectException(String message) {
        super(message);
    }
}

Repository

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을 호출하도록 하여 체크 예외를 처리한다.

위 코드와 같이 기존의 예외를 포함하여 처리해야 스택 트레이스에서 확인이 가능하다.

NetworkClinet

class NetworkClient {
    public void call() {
        throw new RuntimeConnectException("연결 실패");
    }
}

Service

class Service {
    Repository repository = new Repository();
    NetworkClient networkClient = new NetworkClient();

    public void logic() {
        repository.call();
        networkClient.call();
    }
}

Unchecked Exception이기 때문에 throws를 선언하지 않아도 된다.

Controller

class Controller {
    Service service = new Service();

    public void request() {
        service.logic();
    }
}

마찬가지로 Unchecked Exception이기 때문에 throws를 선언하지 않아도 된다.

UncheckedAppTest

@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처럼 예외를 강제로 의존하지 않아도 된다.

profile
Software Developer Lv.0

0개의 댓글