자바 예외 이해 및 예외 추상화
이번 포스팅에서는 예외를 추상화하여, 서비스 계층에서 특정 구현 기술에 의존하지 않고 서비스 계층을 순수하게 유지하는 방법에 대해 설명하겠다. 예외를 추상화하는 방법에 대해 알아보려면 우선 자바 예외에 대해 잘 이해하고 있어야 한다.
자바 예외의 이해
예외 계층
-우선 최상위 에러인 Throwable이 있고 이는 Exception과 Error로 나뉜다.
-Error는 시스템 오류와 같은 애플리케이션에서 복구 불가능한 시스템 예외이기 때문에 개발자는 이를 잡으려 해서 안된다.
-Exception은 체크예외와 언체크예외로 나뉜다. 체크 예외는 컴파일러가 체크하는 예외고 언체크 예외는 컴파일러가 체크하지 않으며, 런타임 오류라고도 부른다. 이 둘의 차이에 대해 자세히 알아보자.
체크 예외
체크 예외는 잡아서 처리하거나, 또는 밖으로 던지도록 선언해야한다. 그렇지 않으면 컴파일 오류가 발생한다.
체크 예외는 컴파일 단계에서 try catch를 통해 잡거나 throws를 통해 예외를 던져야한다. 그렇지 않으면 컴파일 단계에서 오류가 발생하기 때문에 프로그램 자체를 실행할 수 없다. 개발자가 실수로 예외를 누락하지 않도록 컴파일러 단계에서 잡아주는 안전장치 역할을 한다는 장점이 있지만, 모든 체크예외를 잡거나 던져야 하기 때문에, 특정 계층에서 처리할 수 없는 에러까지 모두 신경쓰고 처리해주어야 한다. 이는 의존관계의 문제가 발생하는 원인이 된다.
언체크 예외
언체크 예외는 컴파일러가 예외를 체크하지 않는다.
언체크 예외는 체크 예외와 기본적으로 동일하지만 차이점으로 예외를 던지는 throws 를 선언하지 않고, 생략할 수 있다. 이 경우 자동으로 예외를 던진다. 특정 계층에서 해결할 수 없는 에러나 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있어 의존관계에서 참조하지 않아도 된다는 장점과, 컴파일러 단계에서 잡아주지 않아 에러를 누락할 수 있다는 단점이 있다.
체크 에러 사용시 발생하는 의존관계 문제
위 그림을 보자. 체크 예외인 SQLException과 ConnectException은 데이터베이스에서 발생하는 예외이기 때문에 컨트롤러 및 서비스 계층에서는 이를 처리할 수 없다. 따라서 에러를 밖으로 던져야 하고 이때 (throws SQLException, ConnectException)과 같이 에러를 던지는 코드가 있어야한다. (없으면 컴파일 오류 발생!) 이 때문에 의존관계에 대한 문제가 발생한다. 위 에러들을 던지는 코드에서 java.sql.SQLException과 같은 JDBC 기술에 의존하기 때문에, 추후에 다른 데이터베이스로 변경하는 일이 생기면, 위 코드들을 전부 해당 데이터베이스에 의존하도록 수정해야한다.
언체크 예외 활용을 통한 문제 해결
위 그림에서는 체크에러들을 모두 런타임 에러로 변환하여 사용하였다. 언체크 에러인 런타임 예외를 사용하면 컨트롤러 및 서비스 계층에서 이 예외를 처리할 수 없다면, 별도의 선언 없이 그냥 두면 된다. (컴파일 에러가 발생하지 않는다.) 이에 따라 (throws SQLException, ConnectException)과 같은 JDBC 기술에 의존하는 코드들이 모두 사라지고, 순수한 컨트롤러,서비스 계층을 유지할 수 있다.
예외처리 기본 원칙
- 기본적으로 언체크(런타임) 예외를 사용하자.
- 체크 예외는 비즈니스 로직상 의도적으로 던지는 예외에만 사용하자.
체크 예외의 이런 문제점 때문에 최근 라이브러리들은 대부분 런타임 예외를 기본으로 제공한다. 정말 놓쳐서 안되는 중요한 로직에 대한 예외 정도만 체크예외로 만들어 컴파일러를 통해 이를 인지하는 정도로만 체크 예외를 사용하자.
예외를 전환할 때는 반드시 아래와 같이 기존 예외를 포함해야한다!
catch (SQLException e) {
throw new RuntimeSQLException(e); //기존 예외(e) 포함
}
포함하지 않을 시 예외를 추적할 때 변환되기 전 에러를 추적할 수 없어 정확한 처리를 할 수 없으므로 반드시 포함하여 변환하자!!
스프링 예외 추상화 이해
현재 사용되고 있는 데이터베이스는 여러개가 있고, 각 데이터베이스가 전달하는 오류의 종류 및 오류코드도 수십 수백가지가 있다. 이 모든 상황에 맞는 예외를 직접 코딩한다면, 이는 개발자에게 상당히 고통스럽고 비효율적인 작업이 된다. 스프링은 이런 문제들을 해결하기 위해 데이터 접근과 관련된 예외를 추상화해서 제공한다.
스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층을 제공하고, 각 예외는 특정 기술에 종속적이지 않게 설계되어 있다. 스프링은 예외 변환기를 통해 JDBC, JPA와 같이 대표적인 데이터베이스들을 사용할 때 발생하는 예외를 스프링이 제공하는 예외로 변환해주는 기능도 제공한다.
스프링이 제공하는 예외 변환기
스프링은 데이터베이스에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해주는 변환기를 제공한다. 이것이 가능한 이유는 오류코드들을 미리 정리해둔 xml 파일이 존재하기 때문이다. 파일은 아래 그림과 같이 구성되어있다.

스프링이 제공하는 SQL 예외 변환기는 다음과 같이 사용하면 된다.
SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
translate 메서드의 첫번째 파라미터는 읽을 수 있는 설명이고, 두번째는 실행한 sql, 마지막은 발생된 SQLException 을 전달하면 된다.
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
throw exTranslator.translate("delete", sql, e);
}
실제 코드에 다음과 같이 적용하면 된다!