Spring

[Spring] 스프링의 RuntimeException 추상화

이덩우 2023. 12. 18. 02:20

이전 포스팅에서 체크 예외는 되도록이면 언체크 예외로 변환해 던져야 한다는 것을 배웠다.

실제 코드로 적용해보고, 나아가 스프링의 이런 RuntimeException 추상화에 대해 알아보자.

 


커스텀 런타임 예외 적용

우선 기존 서비스 계층으로 올라오는 JDBC 기술 관련 체크 예외인 SQLException을 처리하기 위해 런타임 예외를 하나 생성하자.

public class MyDbException extends RuntimeException{
    public MyDbException() {
    }

    public MyDbException(String message) {
        super(message);
    }

    public MyDbException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyDbException(Throwable cause) {
        super(cause);
    }
}

이제 리포지토리에서 SQLException이 발생하면 MyDbException으로 변환해 던지면 서비스 계층에 JDBC 관련 코드가 더 이상 누수되지 않는다.

try {
    con = getConnection(dataSource);
    pstmt = con.prepareStatement(sql);
    pstmt.setInt(1, money);
    pstmt.setString(2, memberId);
    int resultSize = pstmt.executeUpdate();
    log.info("resultSize={}", resultSize);
} catch (SQLException e) {
    throw new MyDbException(e);
}

이전 예외 정보를 포함하는 것을 꼭 기억하자.



데이터 접근 예외 직접 만들기

사실 이렇게 MyDbException이라고만 명시적으로 던지면, DB 내부적으로 키가 중복돼서 발생한 예외인지, 문법이 잘못돼서 발생한 예외인지 등등 이름만 보고 알 수 없다.

상용 데이터베이스는 실제로 예외를 반환할 때 어떤 문제 때문에 생긴 예외인지, 오류 코드 정보를 넘겨준다!

우리는 키 중복, 문법 오류 등 디테일한 예외를 만들고 오류 코드에 따라서 다른 예외를 던져줄 수 있다.

키 중복에 대한 상황을 살펴보자. 우선 예외를 하나 만들겠다.

public class MyDuplicateKeyException extends MyDbException{
    public MyDuplicateKeyException() {
    }

    public MyDuplicateKeyException(String message) {
        super(message);
    }

    public MyDuplicateKeyException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyDuplicateKeyException(Throwable cause) {
        super(cause);
    }
}

테스트 코드에서 실제로 H2 DB가 반환하는 키 중복 오류 코드인 23505이면 MyDuplicateKeyException을 반환하는 코드를 작성해보자.

try {
    con = dataSource.getConnection();
    pstmt = con.prepareStatement(sql);
    pstmt.setString(1, member.getMemberId());
    pstmt.setInt(2, member.getMoney());
    pstmt.executeUpdate();
    return member;
} catch (SQLException e) {
    if (e.getErrorCode() == 23505) {
        throw new MyDuplicateKeyException(e);
    }
    throw new MyDbException(e);
}

위 처럼 e.getErrorCode()를 통해 에러 코드가 23505이면 키 중복 예외를 던지고, 아니라면 일반 MyDbException을 던지도록 구현했다.

 

 


스프링의 예외 추상화

문제점이 있다.

실제 데이터베이스의 에러 코드는 너무 많고, 심지어 데이터베이스마다 같은 오류여도 코드가 달라 직접 만들어 쓰기에는 한계가 있다.

이 문제를 해결하기 위해 스프링은 데이터 접근에 관련된 예외를 *추상화*해서 제공한다.

  • DataAccessException : 스프링이 제공하는 데이터 접근 예외의 최상위 계층이다. RuntimeException을 상속받은 언체크 예외이다.
  • NonTransientDataAccessException : 일시적이지 않은 문제이다. 다시 시도해도 실패하는 예외 계층이다.
  • TransientDataAccessException : 일시적일 수 있는 문제이다. 다시 시도하면 해결될 여지가 있다.

스프링은 위와 같은 데이터 접근 예외로 발생한 예외를 변환해주는 *예외 변환기(ExceptionTranslator)*를 제공한다.

스프링의 예외 변환기를 사용하는 코드를 작성해보자.

@Test
void exceptionTranslator() {
    String sql = "select bad grammer";
    try {
        Connection con = dataSource.getConnection();
        PreparedStatement pstmt = con.prepareStatement(sql);
        pstmt.executeQuery();
    } catch (SQLException e) {
        Assertions.assertThat(e.getErrorCode()).isEqualTo(42122);

        SQLErrorCodeSQLExceptionTranslator translator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
        DataAccessException resultEx = translator.translate("tast", sql, e);
        log.info("resultEx", resultEx);
        Assertions.assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
    }
}

예외 변환기에 dataSource를 주입하며 생성하고, 해당 변환기에서 translator.translate()를 호출해 최상위 예외인 DataAccessException 타입으로 반환받을 수 있다.

이로써 데이터 접근에 관련된 대부분의 예외는 스프링이 제공하는 예외 변환기를 통해 편리하게 사용할 수 있게 되었다.

 

 


다음은?

트랜잭션, 예외 누수 문제가 해결되었다.

하지만 현재 리포지토리는 순수 JDBC 기술만을 이용해 DB에 접근하고 있다.

불필요하게 반복되는 코드, 복잡한 코드의 문제를 해결하기 위해 SQLMapper(JdbcTemplate, MyBatis) 혹은 ORM 기술을 사용하며 코드를 개선해나갈 것이다.

참고로 스프링의 예외 변환기를 사용하는 법까지 알아봤지만 JdbcTemplate 혹은 JPA 등을 사용하게 되면 자동으로 예외까지 변환해준다!

평소에 프로젝트를 진행할 때 따로 예외 변환기를 직접 구현하지 않았어도 BadGrammarException 등 예외를 마주친 이유가 바로 자동으로 변환해주기 때문이었다.

지금까지 배운 트랜잭션, 예외에 관한 지식은 내부 원리를 깊이 있게 학습하기에 굉장히 유익한 시간이었다.