Java Exception Handling

exception_hierarchy

예외 클래스의 최상위 클래스는 Throwable 클래스이다.

  1. Error 클래스: 메모리 부족, 시스템 오류와 같이 어플리케이션에서 복구 불가능한 시스템 에러로, 어플리케이션 계층에서 해당 오류를 잡으려고 해서는 안된다.

따러서, Application에서 생각해야되는 예외는, Exception, RuntimeException 이렇게 2가지로 생각해 볼 수 있다.

  1. Exception: 체크 예외라고도 하는데, 해당 에러르 잡지 않으면 컴파일 오류를 일으키게 되므로, 무조건 catch/throw을 통해 예외를 처리해야한다.

  2. RuntimeException: 언체크 예외라고 하는데, 해당 예외는 필수적으로 예외를 처리할 필요가 없다.

어플리케이션에서 예외를 catch하지 않고, 계속해서 throw 하게 되면, 결국에는 main 쓰레드로 넘어가게 되고, 시스템이 종료되게 된다.

웹 어플리케이션의 경우, Controller, Repository, 등등 여러 부분에서 예외가 발생할 수 있는데, 해당 예외를 처리하지 않고 계속 던지게 되면 결국 WAS 서버로 예외가 전달되어, WAS 서버에서 예외를 처리하게 된다. 서버의 경우 예외로 인해 서버가 다운되면 안된다.

Checked Exception

체크 예외 같은 경우, try-catch 구문으로 예외를 처리하거나, throws으로 예외를 바깥으로 던져야한다.

Repository Class

//Exception 클래스를 상속하면서 체크 예외 클래스를 생성한다.
static class MyCheckedException extends Exception {
    public MyCheckedException(String message) {
        super(message);
    }
}
static class Repository {
    public void call() throws MyCheckedException {
        throw new MyCheckedException("ex");
    }
}

가령 위와 같이 체크 예외를 반환하는 클래스가 있다고 가정했을 때, 아래와 같이 2가지 방식 중 1가지 방식으로 무조건 예외를 처리해야한다. 그렇지 않으면 컴파일 에러가 발생한다.

//try-catch 처리
public void callCatch() {
    try {
        repository.call();
    } catch (MyCheckedException exception) {
        log.info("예외 처리, message={}", exception.getMessage(), exception);
    }
}

//throws 처리
public void callThrow() throws MyCheckedException {
    repository.call();
}
  • 장점: 컴파일 단에서 예외를 감지 하기 때문에, 누락되는 예외에 없도록 보장한다.
  • 단점: 모든 체크 예외에 대해서는 반드시 처리 해줘야하므로, 번거로운 부분이 존재한다.

Unhecked Exception

언체크 예외 같은 경우, 의무적으로 예외를 처리해야할 필요가 없다.

Repository Class

//Runtime Exception 클래스를 상속받게 되면 언체크 예외이다.
static class MyUncheckedException extends RuntimeException {
    public MyUncheckedException(String message) {
        super(message);
    }
}
static class Repository {
    public void call() {
        throw new MyUncheckedException("ex");
    }
}

언체크 예외에 대해서는, 예외를 처리하지 않더라도, throws을 통해 예외를 넘기지 않아도 된다.

//try-catch 처리
public void callCatch() {
    try {
        repository.call();
    } catch (MyCheckedException exception) {
        log.info("예외 처리, message={}", exception.getMessage(), exception);
    }
}

//throws 처리
public void callThrow(){
    repository.call();
}
  • 장점: 신경쓰고 싶지 않은 예외에 대해서는 무시할 수 있다. 즉, 모든 예외에 대한 처리를 해야되는 것이 아니다.
  • 단점: 대신, 중요한 비즈니스 로직에서 발생하는 예외와 같은 경우는 예외를 처리해줘야하는데, 이러한 부분에 대해서도 컴파일러로 강제하지 않으므로 누락할 수 있는 부분이 존재한다.

이상적인 예외 처리 방식

  1. 기본적으로 모든 예외에 대해서는 언체크 예외로 관리한다.
  2. 중요한 비즈니스 로직에 대해서는 체크 예외를 처리하도록 한다.
    • 계좌 이체 실패
    • 포인트 부족

보면, 체크 예외는 컴파일러에서 예외를 감지해서 예외를 필수적으로 처리하도록 강제하기 때문에, 얼핏 보면 체크 예외를 쓰는 것이 좋아보인다.

Limitations for Check Error

check_error_propagation

위와 같이, SqlException, ConnectionException 클래스는 체크 예외이다.

Repository 계층에서, 발생한 예외들이 Service, Controller을 거쳐서 WAS 까지 전달되게 된다.

위에 발생한 에러의, 경우, Controller나 Service 계층에서는 처리할 수 없는 예외들이다. 하지만 체크 예외라는 이유로, throws을 통해 계속해서 에러를 전파해줘야한다.

즉, 해당 계층에서 처리할 수 없음에도 불구하고, 체크 예외라는 이유로 계속 넘겨줘야한다. 이렇게 되면 Service, Controller 계층에서는 특정 기술에 의존적인 예외를 throws 해줘야하는 경우가 발생하게 된다.

가령, SQLException의 경우, JDBC에 의존적인 예외 클래스이다. 만약 현재 시스템을 JDBC에서 JPA로 변경하게 된다면 해당 예외 처리 부분을 수정해야하는 문제가 발생한다.

void call_throw() throws Exception{
    ...
}

그럼 위와 같이 Exception 클래스로 하면, 특정 기술에 의존적인 예외 클래스를 직접 명시하지 않아도 되서, 의존적인 예외 처리를 하지 않는 것 처럼 할 수 있다고 생각한다.

하지만, 위와 같이 Exception class로 잡게 되면, 필수적으로 처리해야되는 다른 체크 예외에 대해서도 throws Exception을 통해 외부 메소드로 넘어가게 되면서 해당 예외에 대한 처리가 누락되는 경우가 발생할 수 있다. 따라서, throws Exception을 지정해서는 안된다.

체크 예외를 이용하게 되면 발생하는 2가지 문제점

  • 복구 불가능한 예외 –> 반드시 공통 처리 로직까지 전파해줘야함
  • 의존적인 예외 –> 특정 기술에 의존적인 예외 처리

Using UncheckedError

![unchecked_error_propagation](/assets/images/jsf/Spring_DB/unchecked_error_propagation.png

위와 같이 Unchecked Error을 이용하게 되면, 의무적으로 예외 처리를 하지 않아도 자동으로 예외가 전파되게 된다. 따라서, Service, Controller 계층에서는 자신과 관련 없는 예외에 대해서 처리하지 않아도 된다.

changing_structure_using_unchecked

위와 같이 JDBC에서 JPA로 DA를 바꾸게 된다면, RuntimeSQLException이 RuntimeJPAException으로 변경되게 된다. 이렇게 되면 예외 처리 로직을 수정해야되는데, 언체크 예외로 처리되어 있는 경우, Controller, Service 계층에는 해당 예외를 처리하는 부분이 없어 따로 수정하지 않아도 되고, 공통 처리 로직 부분만 수정하면 되므로, 수정의 여지를 최소화 시킬 수 있다.

일반적으로, 위와 같이 언체크 예외를 이용해서 예외 처리 하는 것이 가장 좋은 방법이다. 다만, 언체크 예외를 활용하는 경우에는 문서화릍 통해 예외에 대한 정의를 명확히 해야한다. –> 필수 처리 로직에 대한 누락 방지를 위해

Exception Stack Trace

기존의 예외를 다른 예외로 변환해야되는 경우, 항상 기존 예외를 포함해서 예외를 변환해야한다.

기존 예외를 포함하는 새로운 예외

public void call() {
    try {
        runSQL(); //SQLException을 발생시키는 메소드
    } catch (SQLException e) {
        throw new RuntimeSQLException(e); //기존 예외(e) 포함
    }
}
13:10:45.626 [Test worker] INFO hello.jdbc.exception.basic.UncheckedAppTest - ex
hello.jdbc.exception.basic.UncheckedAppTest$RuntimeSQLException: java.sql.SQLException: ex at
hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.java:61)at
hello.jdbc.exception.basic.UncheckedAppTest$Service.logic(UncheckedAppTest.java:45) at
hello.jdbc.exception.basic.UncheckedAppTest$Controller.request(UncheckedAppTest.java:35) at
hello.jdbc.exception.basic.UncheckedAppTest.printEx(UncheckedAppTest.java:24)

Caused by: java.sql.SQLException: ex at
hello.jdbc.exception.basic.UncheckedAppTest$Repository.runSQL(UncheckedAppTest.java:66) at
hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.java:59)

기존 예외를 포함시켜서 예외를 변환하는 경우 기존 예외가 발생하는 부분까지 예외 스택을 추적할 수 있다.

기존 예외를 포함하지 않는 경우

public void call() {
    try {
        runSQL();
    } catch (SQLException e) {
        throw new RuntimeSQLException(); //기존 예외(e) 제외
    }
}
[Test worker] INFO hello.jdbc.exception.basic.UncheckedAppTest - ex
hello.jdbc.exception.basic.UncheckedAppTest$RuntimeSQLException: null at
hello.jdbc.exception.basic.UncheckedAppTest$Repository.call(UncheckedAppTest.java:61) at
hello.jdbc.exception.basic.UncheckedAppTest$Service.logic(UncheckedAppTest.java:45)

기존의 SQLException이 발생하는 부분에 대한 예외 스택은 확인할 수 없다. 그렇게 되면 근본적인 예외 처리를 수행할 수 없게 된다.

References

link: inflearn

link:springdb

댓글남기기