checked 예외, unchecked 예외, 예외 전환

도토리·2023년 5월 6일
0

스프링 DB 접근

목록 보기
5/6

자바 예외 파트 복습

  • Error: OutOfMemoryError, StackOverflowError 같이 일단 발생하면 애플리케이션에서 코드로 복구할 수 없기 때문에, 개발자는 Error를 해결하려고 하면 안 된다.
  • Exception과 그 자식 예외(단, RuntimeException과 그 자식 예외 제외): 컴파일러가 예외 처리 여부를 체크하는 checked 예외. 예외 처리하지 않으면 컴파일 오류 발생
  • RuntimeException과 그 자식 예외: 컴파일러가 예외 처리 여부를 확인하지 않는 unchecked 예외. 예외 처리하지 않아도 컴파일 오류는 발생하지 않지만, 만약 예외가 발생하면 런타임 오류 발생

  • 예외 발생 -> (1) 잡아서 처리하거나 (2) 처리할 수 없으면 던진다.
  • 예외를 잡거나 던질 때, 지정한 예외 뿐만 아니라 그 예외의 자식들도 함께 잡히거나 던져질 수 있다. 예를 들어, 메서드 선언부에 'throws RuntimeException'이 선언되어 있는 경우, 메서드 내에서 NoSuchElementException도 던질 수 있다.
  • 웹 애플리케이션에서 예외가 계속 던져지고 결국 처리되지 못하면, WAS가 해당 예외를 받아서 처리하는데, 주로 사용자에게 오류 페이지를 보여준다.

checked 예외

  • Exception과 그 하위 예외(단, RuntimeException과 그 하위 예외는 제외): 컴파일러가 예외 처리 여부를 체크하는 checked 예외
  • checked 예외는 예외 처리하지 않으면, 컴파일 오류 발생
    여기에서 예외 처리란, (1) try-catch문으로 예외를 잡거나 (2) 'throws'로 예외 던진다는 선언을 하는 것
  • 참고로, 'throws Exception'은 anti pattern으로, 메서드 내에서 던질 예외만 명시적으로 선언하는 것이 옳다.

장점

  • 개발자가 실수로 예외 처리하는 것을 누락하지 않도록 해준다. 컴파일러가 안전 장치 역할을 해준다.

단점

  • 모든 checked 예외는 반드시 잡거나 던지도록 처리해야 하는데, 이는 너무 번거롭다.
  • 의존 관계 문제
    예를 들어, repository에서 DB 연결 오류로 SQLException이 발생한 경우, controller까지 SQLException이 전달될 것이다. (repository, service에서 해결할 수 없고, 고객에게 DB 연결 오류가 있다는 것을 알려야 하므로)
    그런데, SQLException은 checked 예외이기 때문에 'throws SQLException'라는 코드를 repository, service에 모두 작성해야 하고, 이에 따라 repository, service는 JDBC에 의존하게 된다. (repository에 JDBC가 아닌 JPA를 사용했다면, SQLException이 발생하지 않음)

unchecked 예외

  • RuntimeException과 그 하위 예외: 컴파일러가 예외 처리 여부를 체크하지 않는 unchecked 예외
  • 예외 처리할 수 없는 경우, 예외를 던지는 throws를 선언하지 않고 생략할 수 있다. 이 경우, 자동으로 예외를 던진다.
  • unchecked 예외 던지기는 주로 생략하지만, 중요한 RuntimeException의 경우, 메서드에 선언하면 해당 메서드를 호출하는 쪽에서 예외 발생 가능성을 인지할 수 있다.
  • 참고) 모든 예외는 잡거나 던져야 한다. 다만, 내가 이를 수행했는지 컴파일러가 알려주냐, 안 알려주냐의 차이만 존재할 뿐!

장점

  • 신경 쓰고 싶지 않은 예외를 무시할 수 있다.
  • 의존 관계 문제 x

단점

  • 개발자가 실수로 예외 처리를 누락할 수 있다.
    -> unchecked 예외는 (1) 문서화를 잘 하거나 (2) 코드에 'throws 예외'를 남기자.

checked 예외 vs unchecked 예외?
예외를 잡을 수 없는 경우 예외를 던지는데, 'throws 예외'를 선언하는가 생략하는가


checked 예외 활용

언제 checked 예외, unchecked 예외를 사용할까?

  • 기본적으로는 unchecked 예외 사용
  • 비지니스 로직상 의도적으로 던지는 + 해당 예외를 반드시 처리해야 하는 경우, checked 예외 사용. EX) 로그인 실패, 계좌 이체 실패, 결제 시 포인트 부족
    개발자가 실수로 예외를 놓치면 안 되는 경우, checked 예외로 만들어, 컴파일러를 통해 예외를 인지하도록 한다.

  • Repository에서 SQLException, NetworkClient에서 ConnectException와 같이 checked 예외 발생했는데, 처리할 수 있는 방법이 없다. 이에 따라 예외를 던지고, 마찬가지로 service, controller에서도 예외를 던진다.
  • 웹 애플리케이션에서는 서블릿 필터, 스프링 인터셉터, 서블릿의 오류 페이지, 스프링 MVC의 ControllerAdvice 등에서 이러한 예외를 공통 처리한다.
    ▶ 사용자에게는 '서비스에 문제가 있습니다.'와 같이 일반적인 메시지를 보여준다. 구체적인 메시지를 보여주어도 일반 사용자는 이해할 수 없고, 또한 보안 문제가 발생할 수 있다.
    ▶ 해결 불가한 공통 예외는 별도의 오류 로그를 남기고, 개발자가 오류를 빨리 인지할 수 있도록 메일, 알림(문자, 슬랙) 등을 전달 한다.

checked 예외의 문제점

  1. 복구 불가능한 예외
    대부분의 예외는 repository, service, controller에서 복구할 수 없다. 예를 들어, SQLException은 SQL 문법에 문제가 있는 경우, DB 서버가 다운된 경우, DB와의 연결에 문제가 있는 경우 등에 발생한다. 이러한 문제는 repository ~ controller에서 해결할 수가 없다.

  2. 의존 관계 문제
    대부분의 예외는 복구 불가능하지만, checked 예외이기 때문에 'throws 예외'를 선언해야 한다. 이에 따라 service, controller는 java.sql.SQLException(= JDBC 기술)에 의존하게 된다.
    향후 repository의 기술을 JDBC -> JPA로 변경하면 예외가 SQLException -> JPAException(실제로 이런 예외 없음!)로 변경되는데, service, controller는 'throws SQLException' -> 'throws JPAException'으로, JPAException에 의존하도록 코드를 변경해야 한다.


TRY: controller, service에서 'throws Exception'하도록 하면, JDBC 기술에 의존하지 않도록 할 수 있다.
-> 다른 checked 예외를 체크할 수 있는 기능 무효화. checked 예외가 발생하더라도 'throws Exception'하기 때문에 컴파일 오류가 발생할 수가 없다.

controller, service는 예외를 처리할 수 없는데, checked 예외이니 throws를 선언해야 한다. 이때, 특정 기술에 의존하게 되는 문제가 발생한다.


unchecked 예외 활용

  • SQLException, ConnectException을 각각 RuntimeException의 자손인 RuntimeSQLException, RuntimeConnectException으로 변경
  • unchecked 예외는 예외를 처리할 수 없다면, throws 선언 없이 그냥 두면 된다.
  • 이제는 controller, service가 예외를 신경쓰지 않아도 된다(= throws 선언 x).
    물론, throws 선언을 하지 않아도 된다는 것이지, RuntimeException는 결국 처리해야 한다.
  • 또한 repository 기술이 변경되는 경우, controller, service는 코드를 변경할 필요가 없다.

문제: 'throws SQLException'. throws 선언 필수 -> JDBC 기술에 의존
해결: throws 선언 생략 가능 -> JDBC 기술에 의존 x


예외 전환

static class Repository {
    public void call() {
        try {
            runSQL(); //checked 예외
        } catch (SQLException e) {
            throw new RuntimeSQLException(e); //기존 예외 포함시키기★
        }
    }

    private void runSQL() throws SQLException {
        throw new SQLException("SQLException 발생");
    }
}

static class RuntimeSQLException extends RuntimeException {
    public RuntimeSQLException() {
    }

    public RuntimeSQLException(Throwable cause) {
        super(cause);
    }
}
  • repository에서 checked 예외가 발생하면, unchecked 예외로 전환해서 예외를 던진다.
  • 예외를 전환할 때는 반드시 기존 예외(checked 예외)를 포함해야 한다.
    기존 예외를 포함해야 stack trace를 출력할 때, 기존 예외도 함께 확인할 수 있다.★
  • 위 코드에서 만약 e를 빼먹었다면, stack trace에서 기존에 발생한 SQLException을 확인할 수 없다. 변환한 RuntimeSQLException부터 예외를 확인할 수 있는 것인데, SQLException이 발생한 이유를 확인할 수 없는 심각한 문제가 발생한다.

참고1. 예외는 원인이 되는 예외를 내부에 포함할 수 있다.

Throwable.java

private Throwable cause = this;

public Throwable(Throwable cause) {
	this.cause = cause;
}

참고2. stack trace 출력(= Exception 종류, 메시지, 발생 위치) 관련
stack trace를 System.out으로 출력하기: e.printStackTrace();
stack trace를 log에 출력하기: log.info()의 마지막 파라미터에 e를 넣어준다.


중요! 예외 전환할 때는 기존 예외를 반드시 포함해야 한다.

0개의 댓글