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

38. 런타임 예외로 문제 해결

sdafdq 2023. 10. 4. 23:18

먼저, 우리가 바꿔 줄 커스텀 예외를 만든다.

public class MyDbException extends RuntimeException{
    public MyDbException() {
    }

    public MyDbException(String message) {
        super(message);
    }

    public MyDbException(String message, Throwable cause) {
        super(message, cause);
    }

    public MyDbException(Throwable cause) {
        super(cause);
    }
}

생성자는 그냥 빈거, 메시지 넣을 수 있는 거, 그리고 바꿔서 던지기 전의 예외가 어디서 부터 온 예외인지 같이 첨부해서 던지기 위해서는 Throwable이 반드시 필요하다.

 

 

그 다음, 서비스가 MemberRepository의 구현체를 의존하고 있었고, 또 JDBC에서 JPA 등 DB 저장소의 환경이 변경 될 가능성을 고려 해(실제라면 잘 없을 것 같고, 처음 만들 때는 여러 상황을 고려 해 인터페이스를 만들 듯.)

MemberRepository 인터페이스를 만든다.

 

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

MemberRepository는 DB와 소통을 위한 객체이므로, crud는 기본으로 들어가 있다.

거기다 런타임으로 바꿔서 던질 거기에, 아예 예외 명시를 하지 않았다.

 

@Slf4j
public class MemberRepositoryV4_1 implements MemberRepository{
    private final DataSource dataSource;

    public MemberRepositoryV4_1(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    public Member save(Member member){
        String sql = "insert into member(member_id, money) 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 new MyDbException(e);
        }finally {
            close(con,pstmt, null);
        }
    }

    @Override
    public Member findById(String memberId){
        String sql = "select * from member where member_id = ?";
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        try {
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, memberId);

            rs = pstmt.executeQuery();
            if(rs.next()){
                Member member =new Member();
                member.setMemberId(rs.getString("member_id"));
                member.setMoney(rs.getInt("money"));
                return member;
            }else{
                throw new NoSuchElementException("member not found memberId = " + memberId);
            }
        } catch (SQLException e){
            throw new MyDbException(e);
        }finally {
            close(con, pstmt, rs);
        }
    }

    private void close(Connection con, Statement stmt, ResultSet rs){
        JdbcUtils.closeResultSet(rs);
        JdbcUtils.closeStatement(stmt);
        DataSourceUtils.releaseConnection(con, dataSource);
    }

    @Override
    public void update(String memberId, int money){
        String sql = "update member set money=? where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;

        try{
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setInt(1, money);
            pstmt.setString(2, memberId);
            int result = pstmt.executeUpdate();
            log.info("result = {}",result);
        }catch(SQLException e){
            throw new MyDbException(e);
        }finally {
            close(con, pstmt,null);
        }
    }

    @Override
    public void delete(String memberId){
        String sql = "delete from member where member_id=?";

        Connection con = null;
        PreparedStatement pstmt = null;
        try{
            con = getConnection();
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1,memberId);
            pstmt.executeUpdate();
        }catch(SQLException e){
            throw new MyDbException(e);
        }finally {
            close(con, pstmt, null);
        }
    }

     public Connection getConnection() throws SQLException {
        Connection con = DataSourceUtils.getConnection(dataSource);

        return con;
    }
}

상속 받아서 만들었다.

내용이 길어 보이지만,

중요한 것은 

catch (SQLException e){
     throw new MyDbException(e);
}

이 부분이다.

예외가 발생할 시 우리가 런타임 예외를 상속받아 만든 예외로 바꾸어서 던진다.

단 그때, 새롭게 생성자로 e(Throwable)를 넣어 전의 예외 정보도 같이 첨부해서 던지게끔 했다.

 

 

자 이렇게 체크예외를 런타임에러로 바꿔줌에 따라,

 

@Slf4j
public class MemberServiceV4 {
    private final MemberRepository memberRepository;

    public MemberServiceV4(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Transactional
    public void accountTransfer(String fromId, String toId, int money){
        Member fromMember = memberRepository.findById(fromId);
        Member toMember = memberRepository.findById(toId);

        memberRepository.update(fromId, fromMember.getMoney() - money);
        validation(toMember);
        memberRepository.update(toId, toMember.getMoney() + money);
    }

    private void validation(Member toMember) {
        if(toMember.getMemberId().equals("ex")){
            throw new IllegalStateException("이체 중 예외 발생");
        }
    }
}

이제 서비스도 특정 기술의 예외에 의존하지 않아도 되게 되었다.

또, MemberRepository가 인터페이스를 사용했기 때문에, 이제 Service는 추상화에 의존하기 때문에,

나중에 MemberRepository를 다른 구현체로 갈아 끼워도 Service의 코드에 손 댈 필요 없이 문제 없다.

 

 

 

 

@Slf4j
@SpringBootTest
public class MemberServiceV4Test {
    private static final String MEMBER_A = "memberA";
    private static final String MEMBER_B = "memberB";
    private static final String MEMBER_EX = "ex";

    @Autowired
    private MemberRepository memberRepository;

    @Autowired
    private MemberServiceV4 memberService;

    @TestConfiguration
    static class TestConfig{
        private final DataSource dataSource;

        public TestConfig(DataSource dataSource) {
            this.dataSource = dataSource;
        }

        @Bean
        MemberRepository memberRepository(){
            return new MemberRepositoryV4_1(dataSource);
        }

        @Bean
        MemberServiceV4 memberSerivce(){
            return new MemberServiceV4(memberRepository());
        }
    }

    @AfterEach
    private void after(){
        memberRepository.delete(MEMBER_A);
        memberRepository.delete(MEMBER_B);
        memberRepository.delete(MEMBER_EX);
    }

    @Test
    @DisplayName("AOP 체크")
    public void aopCheck(){
        assertThat(AopUtils.isAopProxy(memberService)).isTrue();
        assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
    }

    @Test
    @DisplayName("정상 이체")
    void accountTransfer() {
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_B, 10000);
        int transferMoney = 2000;
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney);

        Member findedMemberA = memberRepository.findById(memberA.getMemberId());
        Member findedMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney() - transferMoney);
        assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney() + transferMoney);
    }

    @Test
    @DisplayName("이체 중 예외 발생")
    void accountTransferEx(){
        Member memberA = new Member(MEMBER_A, 10000);
        Member memberB = new Member(MEMBER_EX, 10000);
        int transferMoney = 2000;
        memberRepository.save(memberA);
        memberRepository.save(memberB);

        assertThatThrownBy(()->memberService.accountTransfer(memberA.getMemberId(), memberB.getMemberId(), transferMoney))
                .isInstanceOf(IllegalStateException.class);

        Member findedMemberA = memberRepository.findById(memberA.getMemberId());
        Member findedMemberB = memberRepository.findById(memberB.getMemberId());

        assertThat(findedMemberA.getMoney()).isEqualTo(memberA.getMoney());
        assertThat(findedMemberB.getMoney()).isEqualTo(memberB.getMoney());
    }
}

테스트 코드다. 테스트코드도 마찬가지로 더 이상 예외를 던지거나 처리하지 않아도 된다. 그러함에 따라 SQLException같은 특정 기술의 예외에 의존하지 않아도 되게 되었다. 여기서도 마찬가지로 MemberRepository를 추상체로 의존하고, 주입받을 때만 구현체로 제대로 받았다.

 

IllegalStateException 이것도 런타임 예외다. 이거는 

private void validation(Member toMember) {
    if(toMember.getMemberId().equals("ex")){
        throw new IllegalStateException("이체 중 예외 발생");
    }
}

서비스에서 일부로 예외를 던지게끔 했다.

 

 

 

뭐 나는 꽤 괜찮다고 생각한다.

물론 아직까지도 Repository에 try catch 등 중복적인 부분들이 많고,

 

또 선생님이 설명하는 다른 문제 하나는 예외가 모두 MyDbException으로 같아서 구분할 수 없다는 것이다.

그건 다음 시간에..