ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스프링 예외 추상화
    스프링/스프링 기초DB 2023. 3. 19. 17:00

    예외가 발생했을 때 해당 예외를 그대로 반환하는 것이 아니라 런타임 예외를 따로 만들어서 해당 예외를 반환하는 것이 효율적이다.

     

    @Test
    void duplicateKeySave() {
        service.create("myId");
        service.create("myId"); // 같은 아이디 저장 시도
    }
    
    @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);
        }
    }
    
    @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의 경우
                if (e.getErrorCode() == 23505) {
                    throw new MYDuplicateKeyException(e);
                }
                throw new MyDbException(e);
            } finally {
                JdbcUtils.closeStatement(pstmt);
                JdbcUtils.closeConnection(con);
            }
    
        }
    }

    같은 아이디를 반복 저장하는 상황이다. service에서 반복되는 아이디를 저장한다는 요청이 들어오면 random으로 인해 10000까지의 숫자를 원래 아이디 뒤에 붙여준다. 이 때 에러코드가 23505일 때 MyDuplicateKeyException(e)로 반환하여서 중복 여부를 판단할 수 있다. 이처럼 특별한 예외가 발생했을 때에는 해당 예외처리를 할 수 있는 런타임 예외를 만들어서 반환해주면 된다. 하지만 에러 코드도 db마다 다르고 모든 예외를 저렇게 처리할 수 없으니 스프링에서 지원하는 예외 추상화를 사용하는 것이 좋다.

     

     

     

     

    스프링 예외 추상화

    @Test
    void exceptionTranslator() {
        String sql = "select bad grammar";
    
        try {
            Connection con = dataSource.getConnection();
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.executeQuery();
        } catch (SQLException e) {
            assertThat(e.getErrorCode()).isEqualTo(42122);
    
            SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
            DataAccessException resultEx = exTranslator.translate("select", sql, e);
            // 스프링이 제공하는 SQL 예외 변환기
            
            log.info("resultEx", resultEx);
            assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
        }
    }

    SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);

    DataAccessException resultEx = exTranslator.translate("select", sql, e);

     

    위의 두 줄은 스프링이 제공하는 SQL 예외 변환기이다. 문법 오류가 발생하면 ErrorCode가 42122로 발생하는데 이러한 예외코드들을 db별로 저장해두어서 해당 예외가 발생했을 때 알맞은 예외를 반환해준다. db별로 의존하지도 않기 때문에 추후에 db를 바꿔준다해도 별도의 코드 수정을 최소화로 할 수 있다. 위의 코드는 실제 코드가 아니라 스프링 예외 추상화만을 보기 위한 테스트코드이다.

     

     

     

     

     

    @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) {
            String sql = "insert into member(member_id, money) values (?, ?)";
            // (?, ?)가 아니라 (memberId, money)라 하면 보안상의 위험
    
            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 exTranslator.translate("save", sql, e);
            } finally {
                close(con, pstmt, null);
                // 항상 close가 되도록 finally에서 호출해야 함
            }
    
        }

    원래 코드인 save에 exTranslator.translate 메서드를 통해 스프링 예외 추상화를 진행했다.

Designed by Tistory.