Spring DB : Spring의 예외 추상화 이해

    이 글은 인프런 김영한님의 강의를 복습하며 작성한 글입니다.

    직접 만든 데이터 계층 예외의 문제점

    앞선 포스팅에서 DB에서 발생하는 예외들 중 원하는 예외만 서비스 계층에서 잡아서 처리할 수 있도록 사용자 정의 예외를 만들었다. 그렇지만 이 때 한 가지 문제점이 발생한다. 처리하고 싶은 DB 예외를 만들기 위해서는 각 DB에서 발생하는 예외들을 예외 변환 처리를 해줘야한다. 데이터 접근 계층이 수백개가 되면 이것은 아주 큰 문제로 다가온다. 따라서 이 부분의 해결이 필요해진다. 

     

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

    스프링은 이미 데이터 접근 예외 계층을 추상화해서 제공해준다. 따라서 개발자는 해당 예외 계층을 사용하기만 하면 된다. 

    Spring 제공 예외 계층

    • 스프링은 데이터 접근 계층에 대한 수많은 예외를 정리해서 일관된 예외 계층을 제공해 준다. 
    • 스프링이 제공하는 예외는 순수 특정 기술에 종속적이지 않다. 따라서 서비스 계층에서 스프링이 제공하는 예외를 사용했을 때, DB 접근 기술에 대한 종속성 문제는 해결된다. 
    • 스프링은 JDBC / JPA를 사용할 때 발생하는 예외를 스프링 예외 계층으로 변환해 준다.
    • 스프링 데이터 접근 예외 계층의 최상위 클래스는 DataAcessException이다.예외는 Runtime 예외를 상속받았으며, 따라서 스프링이 제공하는 모든 데이터 접근 계층 예외는 런타임 예외다.
    • DataAcessException의 분류
      • NonTransisentDataAccessException
        • 일시적이지 않은 예외다. 따라서 어플리케이션 단에서는 해결될 방법이 없을 가능성이 높고, 개발자가 의도적으로 개입해서 수정이 필요한 예외다. 예를 들어 잘못된 SQL 구문을 반복한다고 실행이 되진 않는다.
      • TransisentDataAccessException
        • Transisent는 일시적으로 발생하는 Exception을 의미한다. 이 에러는 시간이 조금 흐른 뒤 다시 시도하면, 성공할 가능성이 있는 예외를 의미한다. 예를 들어 락이 걸린 경우, 시간이 조금 흐르면 락이 풀리고 이 때 시도하면 다시 성공할 수 있다.

     

    스프링이 제공하는 DB 접근 예외 살펴보기

    • DataAccessException 클래스를 타고 들어가서 화살표를 클릭해보면, DataAccessException 클래스를 구현한 하위 계층을 확인할 수 있다. 

     

    스프링이 제공하는 예외 변환기

    앞서 스프링은 다양한 DB에서 발생하는 예외를 특정 DB 기술에 종속되지 않는 런타임 예외 계층을 제공해준다고 했다. 그렇지만 이 모든 예외 계층을 개발자가 하나씩 분석해서 만드는 것은 리소스 낭비다. 따라서 스프링은 DB에서 발생하는 예외를 ErrorCode를 바탕으로 스프링이 제공하는 DB 계층 예외로 변경해주는 예외 변환기를 제공해준다. 예외 변환기를 사용하면 예외 변환도 자동으로 되고, 만들어지는 예외 모두 특정 DB 기술에 종속적이지 않기 때문에 DI를 유지하면서 개발 생산성도 높일 수 있다. 

    // 예외 변환기 선언
    public MemberRepositoryV4_2(DataSource dataSource) {
        this.dataSource = dataSource;
        this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    }
    • 예외 변환기는 DB에서 발생하는 ErrorCode를 바탕으로 예외를 변환한다. 따라서 정보를 얻기 위해 반드시 DB Connection이 필요한데, 이 때 DataSource를 넘겨줘서 정보를 받아온다.
    // 예외 변환기 : 예외 변환
    throw exTranslator.translate("find", sql, e);
    • translate를 이용해 발생한 예외를 분석해서 스프링이 제공하는 DB 계층 예외로 변경해준다.
      • Task : 현재 작업하고 있는 내용을 정리하면 된다. 로그에 표시되는 용도
      • SQL : DB에 접근했을 때 사용한 SQL을 넣어준다.
      • Exception : Catch 문에서 잡은 Exception을 넣어준다. 

    • 실행하면 다음 형식으로 로그가 남는다. 여기서 Task [select]라고 되어있는데, 내가 task에 "select"를 입력해서 이렇게 발생한다. 따라서 Log에 남길 값을 Task에 작성해두면 된다. 
    • DataSource에서 얻어온 값을 바탕으로 ErrorCode를 분석해서 필요한 형식의 예외를 만들어준다. 이 때, BadSqlGrammerException으로 예외가 변환된다.

     

    스프링은 어떻게 모든 DB의 예외를 변환할 수 있을까? → 설정 파일 존재함

    sql-error-codes.xml

    • org.springframework.jdbc.support 경로로 가보면 sql-error-codes.xml 파일이 저장되어있다.
      • 이 파일에는 각 DB마다 어떤 ErrorCode가 어떤 Error에 대응되는지를 확인할 수 있다. 
      • 위 이미지에서 볼 수 있듯이 여러 RDBMS(H2, MY-SQL 등)을 지원하는 것을 확인할 수 있다. 

     

    SpringExceptionTranslator를 이용한 테스트 코드 작성

    앞서 사용한 SpringExceptionTranslator를 사용한 테스트 코드를 확인하고자 한다. 

    //SpringExceptionTranslatorTest.java
    
    @Test
    void sqlExceptionErrorCode() {
        String sql = "select bad grammer";
        try {
            Connection con = dataSource.getConnection();
            PreparedStatement pstmt = con.prepareStatement(sql);
            pstmt.executeQuery();
        } catch (SQLException e) {
            assertThat(e.getErrorCode()).isEqualTo(42122);
            int errorCode = e.getErrorCode();
            log.info("errorCode = {}", errorCode);
    
            SQLErrorCodeSQLExceptionTranslator translator
                    = new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException resultEx = translator.translate("select", sql, e);
    
            log.info("resultEx = {}", resultEx);
            assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
    
        }
    }
    • SQLExceptionTranslator를 만들어준다.
    • SQLExceptionTranslator에 발생한 Exception과 SQL을 넘겨주면, 현재 어떤 이유로 Exception이 발생했는지를 분석해서 Spring이 제공하는 DB 계층으로 변경해서 제공해준다. 

    • 실행 결과 다음과 같이 Exception이 BadSqlGrammerException으로 변경된 것을 확인할 수 있다. 

     

     

    SpringExceptionTranslator를 실제 코드에 적용하기 @ MemberServiceV4_2

    앞서 사용했던 SpringExceptionTranslator를 실제 코드에 적용하고자 한다. 

    MemberServiceV4_2 필드 코드 수정

    // MemberServiceV4_2 필드 코드 수정
    private final DataSource dataSource;
    private final SQLExceptionTranslator exTranslator;
    
    public MemberRepositoryV4_2(DataSource dataSource) {
        this.dataSource = dataSource;
        this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    }
    • 먼저 해당 클래스 전체에서 재사용하기 위해 필드 영역 + 생성자에 DI 처리해준다.
    • 앞서 사용방법대로 Translator를 생성할 때 DataSource 정보를 넘겨준다. 
    // MemberServiceV4_2 : 코드 수정 
    @Override
    public Member findById(String memberId){
        String sql = "select * from member where member_id = ?";
    
        ...
        
        } catch (SQLException e) {
            log.error("error", e);
            throw exTranslator.translate("find", sql, e);
        }finally {
            
            ...
            
        }
    }
    • 스프링이 제공하는 예외 변환기를 적용했다.
    • translate를 할 때, Task + Sql + Exception을 넘겨줘서 에러 코드를 분석해서 넘겨주도록 한다. 
    • 이 때, SpringExceptionTranslator가 만들어주는 예외는 런타임 예외이기 때문에 Service 계층에서는 해당 예외를 체크하지 않아도 된다. 

     

    정리

    • 스프링은 데이터 접근 계층에 대해서 일관된 예외 추상화를 제공해준다. 이 때 제공하는 예외는 DB 기술 종속적이지 않으며, 모두 런타임 예외다.
    • 스프링은 예외 변환기를 제공해주고, 예외 변환기는 DataSource / SQL / Exception을 입력받아 ErrorCode에 맞는 적절한 스프링 데이터 접근 예외 계층으로 변환해준다.
    • 스프링 예외 변환기로 만들어지는 예외는 런타임 예외 + 특정 기술 종속적이지 않으므로, 필요 시 Controller / Service 계층에서 사용해도 문제가 없다. 
    • 서비스 계층에서 발생하는 예외를 잡아서 복구해야하는 경우, 예외가 스프링이 제공하는 데이터 접근 계층 예외로 변환되서 넘어오기 때문에 필요한 경우 예외를 잡아서 복구하면 된다.

     

    남은 문제

    지금까지 Connection과 DB를 사용하는 방법을 추상화 했고, Transaction을 추상화했다. 그리고 마지막으로 예외까지 추상화를 했다. 그럼에도 불구하고 코드 상에 해결 여지가 남아있다. 바로 JDBC를 사용하기 때문에 발생하는 반복 문제의 해결이 남아있다. 

    댓글

    Designed by JB FACTORY