스프링 예외 추상화

바그다드·2023년 7월 28일
0

예외

목록 보기
9/9

지난 포스팅까지 자바 예외와 직접 예외를 생성하여 순수 서비스 로직을 유지하는 방법에 대해서 알아보았다.
이번 포스팅에서는 스프링에서 제공하는 예외 추상화에 대해 알아보고 적용해 보려고 한다.

지난 포스팅에서 남아있던 문제는 발생 에러에 따라 예외를 따로 처리하기는 힘들다는 문제가 있었다.
예를 들어 데이터베이스는 여러 제약사항에 따라 각각 다르 에러 코드를 가지고 있고, 또 같은 에러라도 데이터베이스마다 반환하는 코드는 각각 다르다. 그렇다면 개발자는 예외를 잡아 무언가 복구를 시도하려고 하면 데이터베이스별로, 그리고 각 에러별로 해결하는 로직을 수십 수백개를 짜야한다.

이건 아마 전세계 모든 개발자들이 겪는 문제이고, 여러 편리한 기능을 제공하는 스프링답게 스프링에서 이런 예외에 대한 추상화를 제공하고 있어 개발자는 이 기능을 가져다가 사용하기만 하면 된다!!
그럼 스프링 추상화와 적용 방법에 대해 알아보자

스프링 데이터 접근 예외 계층

  • 스프링은 데이터 접근에 대해 여러 예외 계층을 제공한다.
    이 예외들은 특정 기술에 종속되어 있지 않아 JDBC를 사용하든, JPA를 사용하든 스프링이 제공하는 예외만 사용하면 된다.
  • DataAccessException은 2가지로 구분된다.
  1. Transient
    일시적이라는 뜻으로 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있는 예외이다.
    • 예를 들어, 데이터베이스가 일시적으로 상태가 안좋거나, 락이 걸려있을 때를 말한다.
  2. NonTransient
    일시적이지 않다는 뜻으로 동일한 SQL을 다시 시도해도 실패하는 예외이다.
    • 예를 들어, SQL문법에 문제가 있거나, 제약사항을 위반하는 경우를 말한다.

예외 변환기

  • 스프링은 데이터베이스에서 발생하는 오류 코드를 스프링에서 정의한 예외로 자동 변환해주는 변환기를 제공한다.
    코드로 확인하자
@Slf4j
public class SpringExceptionTranslatorTest {

    DataSource dataSource;

    @BeforeEach
    void init() {
        dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
    }
	
    // 변환기 사용 X
    @Test
    void sqlExceptionErrorCode() {
        String sql = "select bad grammer";

        try {
            Connection con = dataSource.getConnection();
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.executeUpdate();
        } catch (SQLException e) {
            assertThat(e.getErrorCode()).isEqualTo(42122);
            int errorCode = e.getErrorCode();
            log.info("errorCode={}", errorCode);
            log.info("error", e);
        }
    }
	
    // 변환기 사용
    @Test
    void exceptionTranslator() {
        String sql = "select bad grammar";

        try {
            Connection con = dataSource.getConnection();
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.executeUpdate();
        } catch (SQLException e) {
            assertThat(e.getErrorCode()).isEqualTo(42122);
            
            SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException resultEx = exTranslator.translate("작업명", sql, e);
            log.info("resultEx", resultEx);
            assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
        }
    }
}
  • translate("기능 설명", sql, exception);
    위의 순서대로 파라미터를 넘겨주면
    DataAccessException라는 최상위 예외를 리턴값으로 생성하는데,
    실제로 테스트로 확인해보면 DataAccessException안에는 BadSqlGrammarException라는 sql문법 오류에 대한 구체적인 예외가 들어있는 것을 확인할 수 있다.

  • 스프링에서는 sql 에러 코드를 파일로 관리하고 있는데,

    이처럼 수 많은 데이터베이스에 대한 에러 코드를 정의하고 있어 지원하는 데이터베이스에서는 종류에 상관없이 발생하는 에러 코드에 대한 예외를 반환해준다.

예외 변환기 적용

그럼 직접 Repository에 예외변환기를 적용해보자.

@Slf4j
public class MemberRepositoryV4_2 implements MemberRepository {

    private final DataSource dataSource;
    private final SQLExceptionTranslator exTranslator;

    public MemberRepositoryV4_2(DataSource dataSource) {
        this.dataSource = dataSource;
        this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    }

    @Override
    public Member save(Member member) {
        // BadSqlGrammarException 발생
//        String sql = "insertxxxx into member(member_id, money) values (?,?)";
        String sql = "insert into member(member_id, money) values (?,?)";

        Connection con = null;
        PreparedStatement pstmt = null;

        try {
            // 생략
        } catch (SQLException e) {
            throw exTranslator.translate("save", sql, e);
        }finally {
            close(con, pstmt, null);

        }
    }
  • 스프링으로 예외를 추상화하고 있기 때문에, 서비스는 Repository의 구현 기술에 의존하지 않고 기술이 변경 되더라도 로직을 유지할 수 있게 되었다.
  • 또한 앞서 직면했던 문제처럼 데이터베이스별로 각각의 에러 코드에 대한 로직을 짤 상관없이 스프링에서 지원하는 예외를 활용해 예외에 따른 처리만 해주면 된다.
  • 다만 이렇게 할 경우, 서비스는 스프링에서 제공하는 기술에 의존하게 되는데, 모든 예외와 데이터 베이스 코드에 대한 로직을 짜는 것보다는 스프링에서 제공하는 추상화에 의존하여 기능을 구현하는 것이 훨씬 효율적이고 안정적일 것이다.

이걸로 서비스가 특정 기술에 의존하는 문제와 데이터베이스 종류에 따른 예외 대응의 문제는 해결이 되었다.
그런데 코드를 보면 데이터베이스에 접근하는 메서드에서 반복이 되는 로직이 눈에 보인다. 커넥션을 얻는 부분이나, PreparedStatement, close()구문 등등은 데이터베이스에 접근하는 로직이라면 중복되는 로직들이다.
이러한 부분은 템플릿을 사용하면 해결이 되는데, 여기서는 JDBC를 사용하고 있으므로 JdbcTemplate을 활용해보자.

JdbcTemplate 활용

@Slf4j
public class MemberRepositoryV5 implements MemberRepository {

    private final JdbcTemplate template;

    public MemberRepositoryV5(DataSource dataSource) {
        this.template = new JdbcTemplate(dataSource);
    }

    @Override
    public Member save(Member member) {
        String sql = "insert into member(member_id, money) values (?,?)";
        // 연결부터 예외처리까지 모두 해결해줌
        template.update(sql, member.getMemberId(), member.getMoney());
        return member;
    }
    // 생략
  • 중복되는 코드가 깔끔하게 제거된 것을 확인할 수 있다.
  • JdbcTemplate은 커넥션 동기화뿐만 아니라, 스프링 예외 변환기도 자동으로 실행을 해준다.

이번 포스팅에서는 스프링에서 제공하는 예외 추상화예외 변환기로 데이터 접근 기술이 변경되어도 서비스 계층의 순수성을 유지할 수 있고, 별도의 예외 클래스를 생성하지 않아도 여러 데이터베이스에서 발생하는 에러 코드에 상관없이 예외를 처리할 수 있게 되었다.
또한 JdbcTemplate을 활용해 중복되는 코드를 제거했을 뿐 아니라, 별도로 예외처리를 해주지 않아도 템플릿에서 예외처리까지 해주는 것을 확인하였다.

출처 : 스프링 DB 1편-김영한

profile
꾸준히 하자!

0개의 댓글