Spring DB : 데이터 접근 예외 직접 만들기

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

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

    앞선 게시글에서는 DB를 접근하면서 발생할 수 있는 체크 예외를 런타임 예외로 감싸는 방법으로 서비스 계층으로 DB 예외가 누수되는 것을 방지했다. 그렇지만 DB 계층에서 발생할 수 있는 특정 예외들 중 일부는 서비스 계층에서 복구 시도를 해볼 수 있다. 

    예를 들어 회원 가입 시, DB에 이미 같은 ID가 있어 유니크 제약 조건에 걸려서 예외가 발생한다고 가정해보자. 이 때, 서비스 계층으로 예외가 올라오면 서비스 계층은 이 ID 뒤에 숫자를 붙여 새로운 ID를 만들어 다시 한번 저장을 시도해볼 수 있을 것이다. 즉, Repository 계층에서 발생할 수 있는 예외를 서비스 계층에서 복구를 해주는 것이다. 

     

    데이터 접근 예외, 어떻게 구분하지? → Error 코드로 구분! 

    JDBC를 예로 들어보자. JDBC는 DB에 접근했을 때 SQLException 예외를 발생시킨다. 이 때, SQLException에는 DB에서 어떤 문제 때문에 Exception이 발생했는지를 알려주는 Error 코드가 들어있다. 개발자는 이 Error 코드를 확인해서, 대처하기 원하는 예외에만 대응하는 Exception 클래스를 만들 수 있다. 

    1. DB가 오류코드를 반환한다.
    2. JDBC 드라이버는 오류 코드를 바탕으로 SQLException을 만들어서 던진다.
    3. Repository는 SQL Exception의 Error Code를 확인해서, 원하는 문제일 경우 런타임 예외로 감싸서 던져준다.

    위와 같은 형식으로 예외 코드를 확인하고 이를 바탕으로 예외를 던져줄 수 있다. 이 때, Repository 계층에서 예외를 감싸서 던져주는 이유는 서비스 계층을 DB 접근 기술 종속으로부터 보호하기 위한 것이다. 체크 예외인 SQLException 전체를 서비스 계층으로 던지면, 서비스 계층은 다시 한번 DB 기술에 종속적으로 변하게 된다. 

     

    DB마다 에러 코드는 다르다! 

    • H2 DB 중복 키 : 23505
    • MySQL DB 중복 키 : 1062

    한 가지 고려해야 할 부분은 같은 이유로 예외가 발생하더라도 각 DB마다 이를 알려주는 에러 코드는 다르다는 것이다. 위에서 볼 수 있듯이, 동일한 키 중복 오류가 발생해도 H2 DB와 MySQL DB가 보여주는 에러 코드는 다르다. 따라서, 어떤 DB를 쓰느냐에 따라서 예외 처리를 다르게 가져가야 한다. 

    지금까지 스프링의 추상화 패턴을 보면 알겠지만, DB마다 다른 예외 코드를 던져주고 이 예외를 감싸야한다는 점 때문에 스프링은 이 예외 코드들의 추상화를 나중에 처리해줄 것이다. 

     

    코드 실습(ExTranslatorV1Test)

    1. 중복키 처리를 위한 런타임 예외를 하나 만든다.
    2. 일부러 DB에서 중복키 에러가 발생하도록 한다.
    3. 중복키 에러가 발생하면 그 예외를 런타임 예외로 전환해서 서비스 계층으로 던진다.
    4. 서비스 계층은 중복키 런타임 예외를 잡아서 복구 시도를 해준다. 

     

    중복키 런타임 예외 생성

    // 기존 MyDbException을 상속
    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);
        }
    
        public MyDuplicateKeyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
            super(message, cause, enableSuppression, writableStackTrace);
        }
    }
    • 중복키 런타임 예외를 만든다.
    • 기존에 만들었던 MyDbException(런타임 예외)를 상속받아 예외를 만든다.
      • 이렇게 예외를 만들면, Runtime DBException이라는 의미있는 계층 구조가 생기기 때문이다. 
      • 이렇게 만든 예외 계층 구조는 JDBC / JPA같은 DB 접근 기술에서 자유로워진다. 따라서 서비스 계층에서도 자유롭게 사용할 수 있게 된다.

     

    Repositpry 계층

    @RequiredArgsConstructor
    static class Repository{
    
        private final DataSource dataSource;
    
        public Member save(Member member) {
            String sql = "insert into member(member_id, money) 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) {
                
                // H2 DB Case // H2 DB의 중복키 에러 코드를 처리.
                
                if (e.getErrorCode() == 23505) {
                    throw new MyDuplicateKeyException(e);
                }
                throw new MyDbException(e);
            }finally {
                JdbcUtils.closeStatement(pstmt);
                JdbcUtils.closeConnection(con);
            }
        }
    • H2 DB는 중복키 예외가 발생할 경우, '23505'라는 예외 코드를 보내준다.
    • Repository는 H2 DB가 주는 중복키 예외 에러 코드를 확인하면, MyDuplicatedException이라는 사용자 설정 런타임 예외로 감싸서 던져준다. 
    • 그렇지 않을 경우, MyDbException이라는 런타임 예외로 감싸서 던져준다. 이를 통해 DB 예외가 서비스 계층까지 누수되는 의존성 문제를 해결한다. 

     

    Service 계층

    @Slf4j
    @RequiredArgsConstructor
    static class Service{
    
        private final Repository repository;
    
        public void create(String memberId){
    
            try {
                repository.save(new Member(memberId, 0));
                log.info("saveId = {}", memberId);
            } catch (MyDuplicateKeyException e) {
                log.info("키 중복, 복구 시도");
                String retryId = generateNewId(memberId);
                log.info("retryId = {}", retryId);
                repository.save(new Member(retryId, 0));
            } catch (MyDbException e) {
                log.info("데이터 접근 계층 예외", e);
                throw e;
            }
        }
    
        private String generateNewId(String memberId) {
            return memberId + new Random().nextInt(10000);
        }
    }
    • Service 계층은 두 가지 예외에 대한 Catch를 처리한다
      1. MyDuplicatedKeyException이 발생하면, 이 예외를 잡아서 새로운 키를 만들어 Member를 다시 저장 시도한다.
      2. MyDbException이 발생하면, 이 예외를 잡아서 단순히 Log만 출력해준다. 
    • 이 때 주의깊게 볼 장면은 두 가지다.
      1. 사실 MyDbException은 Catch로 잡아주지 않아도 된다. 잡지 않으면 자동으로 윗쪽 계층으로 던져진다. 여기서는 로그를 찍기 위해서 만들었다. → 이 계층에서 복구하지 않을 예외는 굳이 잡지 않아도 된다. 예외를 공통으로 처리해주는 부분까지 던지고, 그 부분에서 로그 처리를 해주는 것이 좋다.
      2. Catch는 여러 개를 사용해서 각각 다른 예외를 잡아서 처리할 수 있음을 보여준다. 

     

    테스트 코드

    @Test
    void duplicateKeySave() {
        service.create("myId");
        service.create("myId");
    }
    • 동일한 키로 중복 저장을 한다.
      1. DB에서 중복키 Exception이 발생한다.
      2. Repository 계층에서 중복키 Exception을 Catch 해서 MyDuplicatedKeyException으로 바꿔서 서비스 계층으로 던진다.
      3. Service 계층은 MyDuplicatedKeyException을 잡아서, 다른 Key값으로 Member를 다시 저장해준다.

    다음과 같이 키 중복 에러를 잡아서 복구했던 로그를 확인할 수 있다. 

     

    정리

    1. DB는 예외가 발생하면 예외 코드를 던진다. 그리고 JDBC Driver는 이 예외 코드를 받아서 SQL Exception에 포함시켜 던져준다. 
    2. 개발자는 SQLException에 포함된 예외 코드를 확인해서 원하는 예외인 경우 런타임 예외로 감싸서 던져주면서, 서비스 계층이 DB 접근 기술에 의존하는 문제를 해결하고 더 나아가 원하는 예외만 잡아서 서비스 계층에서 복구를 시도할 수 있도록 해준다.
    3. 여러 Catch 문을 동시에 사용해서 다양한 예외에 대해 각각 다르게 처리해줄 수 있다.
    4. 사용자 예외를 계속 만들어야 한다면, 계층 구조로 형성하는 것을 시도해본다. MyDbException을 만들고, 그 아래에 MyDuplicatedKeyException을 만들어 하나의 의미를 가지는 계층 구조를 형성해본다.
    5. 현재 계층에서 처리할 수 없는 예외는 로그를 남기지 않고, 예외를 공통으로 처리하는 부분으로 던지는 것이 좋다. 

    내용을 정리하면 위처럼 볼 수 있다. 위에 있는 내용을 보면서 느꼈겠지만, 한 가지 문제가 있다는 것을 알 수 있다. 바로 DB마다 같은 에러를 가리켜도 서로 다른 에러 코드를 반환해준다는 점이다. 이런 에러 코드들은 DB마다 많게는 수만건이 있는데, 개발자가 모든 DB의 모든 에러 코드에 대해 대응할 수 없다. 

    다행히도 스프링은 이런 예외 코드를 추상화해서 예외를 만들어주는 기능을 제공한다. 

    댓글

    Designed by JB FACTORY