Spring DB : 스프링과 문제 해결, 예외 처리 / 반복

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

    체크 예외

    서비스 계층은 가급적 특정 구현 기술에 의존하지 않고, 순수한 자바 코드로 작성되는 것이 좋다. 이렇게 하려면 예외에 대한 의존 문제도 해결해야한다.

    MemberServiceV3_3

    예를 들어 위의 코드를 볼 수 있다. 위는 MemberServiceV3_3 코드인데, 메서드에 "throws SQLException'이 있는 것을 확인할 수 있다. @Transcational을 이용해 트랜잭션 추상화로 DB 기술 의존성을 해결한 것으로 이해를 했으나, 사실은 SQLException이 남아있기 때문에 아직까지 DB 기술에서 자유롭지는 않다. 

    앞서 배웠던 예외들을 살펴보면 이 SQLException은 Service 계층에서 처리할 수 없는 문제다. 즉, 서비스 계층이 신경쓰지 않아도 되는 문제다. 따라서 Repository 계층에서 SQLException을 런타임 예외로 바꿔서 던져주면 깔끔하게 DB 기술 의존성 문제가 해결된다. 

    체크 예외와 인터페이스

    DI를 잘 처리해주기 위해 Service 계층이 MemberRepository 인터페이스에 의존하고, 각 DB 기술은 인터페이스의 구현체를 구현하는 것으로 접근을 하려고 한다. 그런데 여기서 의문이 발생한다. 왜 아직까지 인터페이스를 만들고 있지 않았던 걸까? 그 이유는 SQLException의 종속성 문제를 해결하지 못하고 있었기 때문이다.

    SQLException이 해결되지 않는다면 인터페이스의 메서드에도 "throws SQLException"이 명시되어야 한다. 만약 인터페이스에서 이렇게 명시되어 있지 않다면 실제 구현체에서 'throws SQLException'이 들어간 메서드는 오버라이드 된 것이 아니라, 별개의 메서드가 만들어진다. 따라서 반드시 인터페이스에 'throws SQLException'을 해줘야한다. 그런데 이렇게 인터페이스 자체가 특정 DB 기술에 종속적인 인터페이스가 된다.

     

    특정 기술에 종속적인 인터페이스

    DB 구현 기술을 쉽게 변경하기 위해 인터페이스를 도입했다. 그렇지만 SQLException 같은 특정 구현 기술에 종속적인 체크 예외를 사용하게 되면, 인터페이스의 메서드에도 해당 예외가 포함되어야 한다. 즉, 기술 종속적인 인터페이스가 된다. 이 인터페이스는 향후 JDBC → JPA로 기술이 변경되면, 인터페이스 + 모든 구현체의 코드 수정이 필요해진다. 

     


    런타임 예외와 인터페이스 

    SQLException은 체크 예외였기 때문에 각 메서드에 해당 예외 처리에 대한 부분이 명시되어야했다. 이 SQLException을 런타임 예외로 한번 감싸줄 경우, 인터페이스는 예외에서 자유로워지게 된다. 왜냐하면 런타임 예외는 처리 방법을 명시하지 않아도, 알아서 상위 객체로 던져주기 때문이다. 런타임 예외의 이런 성질을 사용하게 되면 DB 기술에 자유로운 인터페이스를 사용할 수 있게 된다. 


    런타임 예외와 인터페이스

    런타임 예외는 이런 부분에서 자유롭다. 인터페이스에 런타임 예외를 따로 선언하지 않아도 된다. 따라서 인터페이스가 특정 기술에 종속적이지 않게 된다.

    코드 개선

    (MemberRepository / MemberRepositoryV4_1 / MemberServiceV4 / MemberServiceV4Test / MyDbException)

    여기서는 총 2가지 일을 처리한다.

    1. MemberRepository 인터페이스를 만들어, 자유롭게 DI를 할 수 있도록 개선한다.
    2. MemberRepository에서 발생하는 예외는 MyDbException(런타임 예외)로 감싸서 던져준다.

     

    MemberRepository

    public interface MemberRepository {
    
        Member save(Member member);
        Member findById(String memberId);
        void update(String memberId, int money);
        void delete(String memberId);
    
    }
    • MemberRepository 인터페이스를 작성한다.
    • throws SQLException이 없는 것을 확인한다.

     

    MyDbException

    public class MyDbException extends RuntimeException{
    
    ...
    
    }
    • MyDbException을 만들어준다.
    • 런타임 예외로 만들어준다.

     

    MemberRepositoryV4_1

    @Override
    public Member findById(String memberId){
        String sql = "select * from member where member_id = ?";
    
        PreparedStatement pstmt = null;
        ResultSet rs = null;
    
        try {
    
            Connection conn = getConnection();
            pstmt = conn.prepareStatement(sql);
            pstmt.setString(1, memberId);
    
            rs = pstmt.executeQuery();
            log.info("Connection = {}, class = {}", conn,conn.getClass());
    
            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) {
            log.error("error", e);
            // ***** 중요 포인트 *****
            throw new MyDbException(e);
        }finally {
            JdbcUtils.closeResultSet(rs);
            JdbcUtils.closeStatement(pstmt);
        }
    }
    • MemberRepository 인터페이스를 구현하도록 한다.
      • 각 메서드를 @Override 처리한다.
    • SQLException을 잡아서 MyDbException으로 바꿔서 던져준다.
      • MyDbException은 런타임 예외이기 때문에 실제 메서드에 어떠한 Exception도 표현하지 않아도 된다.
      • MyDbException 생성 시, 반드시 기존 예외를 감싸서 던진다. 이렇게 해야 StackTrace에 정상적으로 로그가 출력된다.

     

    MemberServiceV4

    public class MemberServiceV4 {
    
        private final MemberRepository memberRepository;
    
        ...
    
        private void bizLogic(String fromId, String toId, int money){
            Member fromMember = memberRepository.findById(fromId);
            Member toMember = memberRepository.findById(toId);
    
            memberRepository.update(fromId, fromMember.getMoney() - money);
            validate(toMember);
            memberRepository.update(toId, toMember.getMoney() + money);
        }
    }
    • MemberServiceV4는 MemberRepository 인터페이스에 의존하도록 한다.
    • MemberRepository에서 런타임 예외를 던지기 때문에 기존 버전의 메서드에 존재하던 "throws SQLException"이 사라졌다. 
      • Repository → Service로 Exception 누수가 해결됨.



    MemberServiceV4Test

    @TestConfiguration
    @RequiredArgsConstructor
    static class TestConfig {
    
        private final DataSource dataSource;
    
        @Bean
        MemberRepository memberRepository() {
            return new MemberRepositoryV4_1(dataSource);
        }
    
        @Bean
        MemberServiceV4 memberServiceV4() {
            return new MemberServiceV4(memberRepository());
        }
    
    }
    • MemberServiceV4는 다음과 같이 등록해주는 스프링 빈만 변경해준다.

     

    정리

    • 체크 예외(SQLException)을 런타임 예외(MyDbException)으로 변경하면서, Repository 계층의 예외가 서비스 계층으로 누수되지 않도록 했다. 덕분에 Service 계층은 순수한 자바 코드로 작성할 수 있게 되었다.
    • Service 계층은 DB 기술에서 독립적으로 되었다. 따라서 향후 DB 기술에 따라 다른 Repository 구현체가 들어와도 Service 계층의 코드 변형은 없다. 

     

    남은 문제

    Repository에서 넘어오는 특정한 예외들 중 일부는 Service 계층에서 복구를 시도해볼 수 있다. 예를 들어 Key값이 중복되어 발생하는 유니크 제약 조건 예외는 Service 계층에서 복구를 시도해볼 수 있다. 그런데 지금 방식은 MyDbException 예외만 넘어온다. 그렇기 때문에 예외를 구분할 수 없다는 단점이 있다. 

    만약 특정 상황에서 발생하는 예외는 잡아서 복구하고 싶을 때, 이럴 때는 어떻게 처리할 수 있을까?

    댓글

    Designed by JB FACTORY