자바 기본 예외

예외는 크게 Checked Exception, Unchecked Exception로 나뉜다.

여기서 말하는 “체크”의 주체는 프로그래머가 아닌 컴파일러이다. Checked Exception은 컴파일러가 체크하고 만약 누락되었다면 컴파일이 되지 않는다. 반면에 Unchecked Exception은 누락해도 컴파일은 잘 되지만, 실행 도중에 발생하여 잡지 못했다면 그대로 프로그램이 죽어버린다. 그래서 이를 다른 말로 런타임 에러라고도 한다.

Checked ExceptionUnchecked(Runtime) Exception
컴파일 에러 여부OX
대표 예외Throwable, IOExceptionError, NullPointerException
스프링 트랜잭션에서의 기본값CommitRollback
처리 여부반드시 처리하거나, 명시적으로 던져야 한다.명시적인 처리를 강제하지 않는다.

체크 예외와 런타임 예외

처음에는 체크 예외가 컴파일에서 잡아주기 때문에 더 나은 방식이라고 생각했다. 그래서 자바가 기본으로 제공하는 기능들에는 체크 예외가 많다. 그런데, 시간이 흐르면서 사용하는 라이브러리가 많아지고 여러 패턴의 등장으로 인해 코드가 잘게잘게 쪼개어지면서 온갖 군데에서 예외가 던져지기 시작했다. 그런데 개중에서는 해당 레이어에서 해결할 수 없거나 복구할 수 없는 예외도 끼어있기 일쑤였다. 그래서 어떤 개발자는 throws Exception이라는 극약처방(독약)을 마시기도 했다.

또한, 체크 예외는 메서드에 명시적으로 던져야되기 때문에 특정 라이브러리에서 제공하는 예외를 코드에 작성하게 되면 해당 객체는 특정 라이브러리에 의존적인 객체가 되어버린다. 만약, 라이브러리가 변경되거나 삭제되면 해당 예외를 적은 모든 코드에 가서 수정을 해줘야했다.

위의 문제로 인해 스프링과 JPA을 포함한 최신 라이브러리들은 대부분 런타임 예외를 기본으로 제공한다.

런타임 예외의 주의점

/**
 * 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;

런타임 예외의 특성상 컴파일이 잡아주지 않기 때문에 놓치게 되면 바로 톰캣까지 예외가 올라가게 된다. 이 때문에 런타임 예외를 적용하게 된다면 그게 무엇인지 문서화를 잘 해두어야한다.

public void call() {
    try {
        runSQL();
    } catch (SQLException e) {
        throw new RuntimeSQLException(e);
    }
}

그리고 만약 기존의 체크 예외를 잡아서 자신만의 런타임 예외로 변경하는 작업을 진행하게 된다면 생성자의 Throwable cause기존의 체크 예외를 넣어주는 것을 잊지 말자.

스프링 데이터 접근 예외

DataAccessException

스프링은 특정 기술에 종속적이지 않게 설계한 예외들을 제공한다. 즉, 서비스 계층에서도 이 예외를 사용할 수 있다는 의미이며, JDBC나 JPA를 사용할 때 발생하는 예외를 위의 예외로 변환해주는 객체 또한 지원한다.

스프링 데이터 예외의 최상위 객체는 DataAccessException이며 이는 RuntimeException을 상속받은 런타임 예외이다. DataAccessException은 예외의 특성에 따라 크게 NonTransient, Transient로 나뉜다.

Transient는 일시적인 에러라는 의미로, 예외가 발생한 SQL 쿼리문을 다시 시도했을 때 성공할 가능성이 있다는 뜻이다. 대표적으로 쿼리 타임아웃, DB Lock과 관련된 오류들이 있다.

NonTransient는 그와 반대로 영구적인 예외라는 의미로, 예외가 발생한 쿼리문을 재전송해도 무조건 실패하게 된다. 대표적으로 SQL 문법 오류, 데이터베이스 제약조건 위배 등이 있다.

예외 변환기

package org.springframework.jdbc.support;

@FunctionalInterface
public interface SQLExceptionTranslator {

    @Nullable
    DataAccessException translate(String task, @Nullable String sql, SQLException ex);
}

SQLExceptionTranslator는 SQLException을 DataAccessException으로 변환해준다. 해당 인터페이스의 구현체는 SQLErrorCodeSQLExceptionTranslator이다.

translate의 각 인자는 다음과 같다.

  • String task: 읽을 수 있는 설명
  • String sql: 실제로 실행한 SQL 쿼리문
  • SQLException e: 실제로 발생한 예외

sql-error-codes.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
<!--org.springframework.jdbc.support.sql-error-codes.xml-->

<beans>
    <bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
        <property name="badSqlGrammarCodes">
            <value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value>
        </property>
        <property name="duplicateKeyCodes">
            <value>23001,23505</value>
        </property>
        <property name="dataIntegrityViolationCodes">
            <value>22001,22003,22012,22018,22025,23000,23002,23003,23502,23503,23506,23507,23513</value>
        </property>
        <property name="dataAccessResourceFailureCodes">
            <value>90046,90100,90117,90121,90126</value>
        </property>
        <property name="cannotAcquireLockCodes">
            <value>50200</value>
        </property>
    </bean>

    <bean id="PostgreSQL" name="Postgres" class="org.springframework.jdbc.support.SQLErrorCodes">
        <property name="useSqlStateForTranslation">
            <value>true</value>
        </property>
        <property name="badSqlGrammarCodes">
            <value>03000,42000,42601,42602,42622,42804,42P01</value>
        </property>
        <property name="duplicateKeyCodes">
            <value>21000,23505</value>
        </property>
        <property name="dataIntegrityViolationCodes">
            <value>23000,23502,23503,23514</value>
        </property>
        <property name="dataAccessResourceFailureCodes">
            <value>53000,53100,53200,53300</value>
        </property>
        <property name="cannotAcquireLockCodes">
            <value>55P03</value>
        </property>
        <property name="cannotSerializeTransactionCodes">
            <value>40001</value>
        </property>
        <property name="deadlockLoserCodes">
            <value>40P01</value>
        </property>
    </bean>
</beans>

스프링 SQL 예외 변환기는 SQLException.Code를 이 파일에 대입하여 어떤 스프링 데이터 접근 예외로 전환할지 찾아낸다. 예를 들어서 H2에서 42000이 발생하면 이 파일에 대조한 다음, badSqlGrammarCodes를 받아내어 최종적으로는 BadSqlGrammerException을 반환하게 된다.

profile
백엔드 개발자 지망생

0개의 댓글