ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 스프링의 예외 처리
    김영한의 스프링/데이터베이스 2024. 10. 2. 18:58

    이전 포스트에서 트랜잭션 관련 문제를 해결하였지만 마지막 예외 누수 문제에 대해 해결하지 못하였다. 간단히 요약하면 다음과 같이 서비스 계층은 리포지토리가 던지는 SQLExeption 예외를 의존하게 된다.

     

    - 문제의 Service 예외 누수

    @Transactional
    public void accountTransfer(String fromId,String toId,int money) throws SQLException {
         bizLogic(fromId,toId,money);
    }

     

    해당 예외 의존 때문에 만약 Repository의 인터페이스를 만들어도 문제가 발생한다. 

     

    public interface MemberRepositoryEx {
        Member save(Member member) throws SQLException;
        Member findById(String memberId) throws SQLException;
        void update(String memberId, int money) throws SQLException;
        void delete(String memberId) throws SQLException;
    }

     

    구현 기술을 쉽게 변경하기 위해 인터페이스를 도입하더라도 SQLException 같은 특정 구현 기술에 종속적인 체크 예외를 사용하게 되면 인터페이스에도 해당 예외를 포함해야한다. 하지만 이것은 우리가 원하는 순수한 인터페이스가 아니다. 향후 JDBC가 아닌 다른 기술을 변경한다면 인터페이스 자체를 변경해야한다.

     

    해결하는 방법은 간단하다. 레포지토리에서 SQLException을 잡고 런타임 예외 즉, 언체크 예외를 던지게 하면 된다.

     

    체크 예외 해결 방법

    @Override
    public Member save(Member member) {
        String sql="insert into member values(?,?)";
    
        Connection con=null;
        PreparedStatement pstmt=null;
    
        try {
            con= getConnection();
            pstmt=con.prepareStatement(sql);
            pstmt.setString(1,member.getMemberId());
            pstmt.setInt(2,member.getMoney());
            pstmt.executeUpdate();
            return member;
        } catch (SQLException e) {
        	// RuntimeException을 상속 받은 커스텀 예외로 다시 예외를 던짐
           throw new MyDbException(e);
        } finally {
            close(con,pstmt,null);
        }
    }

     

    이렇게 체크 예외를 언체크 예외로 던지면 예외에 대한 의존성을 제거할 수 있다. 여기서 의문점이 하나가 있다. 모든 예외가 SQLException인데 만약에 특정 예외에 대해 복구를 하고 싶을 땐 어떻게 해야할까? 예를 들어서, 사용자가 아이디를 중복된걸 입력을 했을때 서비스 층에서 아이디에 추가적인 랜덤 문자를 붙여서 다시 데이터베이스 저장을 하도록 하게 하려면 해당 예외는 따로 커스텀해서 레포지토리 계층에서 던지고 해당 예외에 대해 서비스 층에서 해결하게 해야한다. 그렇다면 SQLException을 던지는데 어떻게 구별을 해야할까?

     

    SQLException

    SQLException 안에는 데이터 베이스가 제공하는 errorCode가 들어있다. errorCode를 확인하면 데이터 베이스에서 어떤 문제가 발생했는지 알 수 있다.

     

    H2 데이터 베이스 예

    • 23505 : 키 중복 오류
    • 42000 : SQL 문법 오류

    - ErrorCode를 사용해 특정 오류 던지기

    public Member save(Member member){
         String sql="insert into member values(?,?)";
    
         Connection con=null;
         PreparedStatement pstmt=null;
    
         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);
          }finally {
               JdbcUtils.closeStatement(pstmt);
               JdbcUtils.closeConnection(con);
          }
    }

     

    이렇게 특정 오류 코드에 대해서는 따로 커스텀한 언체크 예외를 던지게 만들고 서비스 계층에서 해당 예외를 잡아서 처리하면 된다. 근데 여기서 문제가 있다. 현재는 H2 데이터 베이스를 사용하고 있어서 키 중복에 대한 예외 코드가 23505이지 만약 MySQL을 사용한다면 같은 오류여도 코드가 달려져 코드를 따로 수정해야한다. 또한 내가 이 예외 말고 처리하고 싶은 예외가 있다면 하나씩 전부 체크하여 던지는 코드를 작성해야한다... 엄청 귀찮은 작업..ㅜ 이를 해결하기 위해 스프링은 데이터 접근과 관련된 예외를 추상화해서 제공한다.

    스프링 예외 추상화 이해

    지금까지 우리는 특정 상황 예외에 대해 직접 런타임 예외 즉, 언체크 예외로 커스텀 마이징하여 사용하였다. 이를 해결하기 위해 스프링은 예외를 저일해서 일관된 예외 계층을 제공한다. 

     

    각각의 예외는 특정 기술에 종속적이지 않게 설계되어 있다. 따라서 서비스 계층에서도 스프링이 제공하는 예외를 사용하면 된다. 예를 들어서 JDBC를 사용하든 JPA를 사용하든 스프링이 제공하는 예외를 사용하면 된다. 여기서 알 수 있는 사실은 런터임 예외를 상속 받았기 때문에 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다. 또한 DataAccessException은 두가지로 구분하는데 NonTransient와 Transient이다. Transient는 일시적이라는 뜻으로 SQL을 다시 실행했을 때 성공할 가능성이 있다는 뜻이다. 예를 들어, 락 관련 오류가 있다. NonTransient은 반대로 일시적이지 않다는 뜻으로 같은 SQL을 다시 실행해도 무조건 실패한다. 예를 들어 SQL 문법 오류가 있다.

     

    추상화된 예외를 제공해서 우리가 직접 커스텀 마이징할 필요가 없지만 한 가지 문제가 남아있다. 바로 우리가 직접 ErrorCode를 판별해서 해당 추상화된 예외로 바꿔야한다. 게다가 이전에 언급했듯이 데이터 베이스마다 ErrorCode가 다르기 때문에 데이터 베이스 자체를 변경하면 우리는 코드도 바꿔야한다. 이를 또한 스프링이 해결해준다!!

     

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

    ErrorCode를 직접 확인하고 하나하나 스프링이 만들어준 예외로 변경해주는 것은 현실성이 없어 스프링은 예외 변환기를 제공한다.

    - SQLExceptionTranslator 적용

    public class MemberRepository implements MemberRepository{
    
        private DataSource dataSource;
        // 스프링 예외 변환기
        private SQLExceptionTranslator translator;
    
        public MemberRepository(DataSource dataSource) {
            this.dataSource = dataSource;
            translator=new SQLErrorCodeSQLExceptionTranslator(dataSource);
        }
    
        @Override
        public Member save(Member member) {
            String sql="insert into member values(?,?)";
    
            Connection con=null;
            PreparedStatement pstmt=null;
    
            try {
                con= getConnection();
                pstmt=con.prepareStatement(sql);
                pstmt.setString(1,member.getMemberId());
                pstmt.setInt(2,member.getMoney());
                pstmt.executeUpdate();
                return member;
            } catch (SQLException e) {
            // 스프링 예외 변환기를 사용하여 어떤 예외인지 판단하고 해당 예외를 스프링 예외로 바꿔서 던짐
                throw translator.translate("save",sql,e);
            }finally {
                close(con,pstmt,null);
            }
        }
    	... 생략
    }

     

    translate() 메서드의 첫번째 파라미터는 읽을 수 있는 설명이고, 두번째는 실행한 sql, 마지막은 발생된 SQLException을 전달하면 된다. 이렇게 하면 자동으로 SQLException의 ErrorCode에 맞는 적절한 스프링 데이터 접근 예외로 변환해준다. 만약 서비스, 컨트롤러 계층에서 예외 처리가 필요하다면 특정 기술에 종속적인 SQLException을 사용하는 것이 아니라, 스프링이 제공하는 데이터 접근 예외를 사용하면 된다. 만약 JPA 기술로 바꾼다 하더라도 같은 방법으로 스프링 예외를 던지게 하면 되므로 서비스는 완전히 독립적이게 된다. 물론 스프링이 제공하는 예외를 사용하기 때문에 스프링에 대한 기술 종속성은 발생한다. 하지만 이는 예외를 직접 정의하고 예외 변환도 직접하는 것에 대한 시간에 대한 트레이드 오프라고 생각하면된다.

    '김영한의 스프링 > 데이터베이스' 카테고리의 다른 글

    [Spring] 스프링 트랜잭션 전파  (0) 2024.10.04
    [Spring] 스프링의 트랜잭션 문제 해결  (1) 2024.10.02
    트랜잭션  (0) 2024.09.12
    커넥션 풀과 데이터소스  (0) 2024.09.11
    JDBC  (0) 2024.08.31
Designed by Tistory.