스프링/5. 스프링 DB-1

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

sdafdq 2023. 10. 5. 13:20

아마도 SQLException 이런 걸 직접 만든다는 뜻 같다.

 

DB오류에 따라서 사실 복구 가능한 경우는 많이 있지는 않지만,

복구 하고 싶은 경우가 있다.

 

예를 들면 계정을 새로 생성하는데 DB에 이미 같은 id가 있으면 이 id뒤에 뭔가를 덧붙여 만드는..

 

그 외 우리가 id확인이나 닉네임 확인 할 때 같은 거 있으면 뒤에 뭐 붙여서 추천해주는 그런 경우 있는데, 그런건가봄.

 

 

 

날리는 오류코드도, 같은 SQLException이지만 오류의 종류에 따라 오류 코드 같은 것을 보내준다.

이것도 DB에 따라 다르긴 한데, H2같은 경우는 Primary Key를 사용한 것에 같은 걸 생성하려 든다면 23505이다. 42000은 sql 문법 오류.

 

이건 e.getErrorCode() 해보면 읽을 수 있다.

 

그럼 뭐 예를 들어 e.getErrorCode() == 23505 이런 식이면

 

이미 존재하는 아이디입니다! 

추천 아이디 : 

Member.memberId + '12345'

 

이런 식으로 응답하게끔 하거나 뭐 그런..

 

 

 

@Slf4j
public class ExTranslatorV1Test {
    Repository repository;
    Service service;

    @BeforeEach
    public void init(){
        DriverManagerDataSource dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
        repository = new Repository(dataSource);
        service = new Service(repository);
    }

    @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 + UUID.randomUUID().toString().substring(0,5);
        }
    }



    @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){
                if(e.getErrorCode() == 23505){
                    throw new MyDuplicateKeyException(e);
                }
                throw new MyDbException(e);
            }finally {
                JdbcUtils.closeStatement(pstmt);
                JdbcUtils.closeConnection(con);
            }
        }
    }
}

그런 식으로 만들어 봄.

먼저 리포지토리,

Member를 받아서 저장을 시도 하는데,

만약 에러가 뜨고, 마침 그 에러의 에러코드가 23505(H2의 Primary Key 중복)이면 MyDuplicateKeyException 이라는 에러로 던짐. 그 외에는 그냥 MyDbException으로 던짐. 그러니까 MyDuplicateKeyException은 내가 이건 특별히 처리할 것이다 라는 것을 구분하기 위해 저 23505가 에러코드면 MyDuplicateKeyException로 에러를 던진다.

 

그래서 @Test에 보면, 저렇게 같은 이름으로 생성시도를 두번하는데, 당연히 member_id부분은 DB의 테이블을 생성했을 때 primary key로 해놔서 에러를 내놓을 것이고, H2의 Primary Key의 중복 에러의 코드는 23505라고 했기에, 그 코드를 읽고 MyDuplicateKeyException을 던질 것이다.

 

그럼 그것이 service로 올라갈 것이고, 거기서 잡은 예외가 MyDuplicateKeyException이면 memberId에 UUID의 일부분을 덧붙여 다시 DB에 save를 시도해본다.

 

그 외에 MyDbException도 한번 잡아보고, 저렇게 catch 여러 개 할 수  있다.

물론 MyDbException 저렇게 굳이 잡아서 재 던지지 않아도 런타임에러라 굳이 명시 안해줘도 알아서 위로 간다.

 

 

 

MyDuplicateKeyException은 우리가 RuntimeException을 상속받아 만든 에러이기 때문에 서비스는 여전히 순수성을 유지할 수 있다.

 

그래서 뭐 당연한 말이지만, 예외변환은 리포지토리에서 일어나야 함.

 

 

아직 남은 문제는, 저런 primary key 중복 오류코드 같은 거 DB마다 다르다.