스프링 DB 접근 1편 - 예외 처리

이성준·2022년 9월 29일
0

Spring

목록 보기
9/11

김영한님 스프링 DB 접근 1편 - 예외처리부분을 보고 정리한 내용입니다.

예외처리

프로그램이 실행중 어떤 원인에 대해서 오작동을 하거나 비정상적으로 종료되는 경우가 있는데, 이러한 결과를 초래하는 원인을 에러 또는 오류라고 한다.
그리고 그 오류는 크게 세개로 나눌수 있다.

컴파일 에러

말 그대로 컴파일을 할때 발생하는 에러이다. 컴파일 에러는 엥간하면 IDE에서 잡아주고(빨간줄 표시) 고치지 않으면 실행자체가 되지않기 때문에 개발자가 인식하기 편하다.

런타임 에러

런타임 에러는 실행하고 나서 발생하는 오류인데. 컴파일러가 이런 잠재적인 오류까지는 검사하지 못해서 발생한다. 자바에서는 이런 런타임에러를 또 두 분류로 나눴는데 바로 예외와 에러이다.

  • 예외 : 예외는 이제 우리가 수습할 가능성이 있는 미약한 오류를 말하는것이고
  • 에러 : 에러는 이제 메모리부족이나 스택오버플로우같은 복구할 수 없는 심각한 오류를 말한다.

예외클래스 계층 구조


모든 예외는 Exception클래스의 자손이며 크게 그중에서도 RuntimeException을 상속받은 언체크 예외, 나머지를 체크 예외라고 부른다.

  • 언체크 예외 : RuntimeException의 자손들은 컴파일러가 체크하지않고, 거의 대부분 개발자의 실수로 발생하는 예외이다. 말그대로 컴파일러가 예외를 체크하지 않기 때문에 throw를 따로 선언하지 않아도 자동으로 예외를 던져준다. 또한 복구가 거의 불가능하다. ex) ClassCastException
  • 체크 예외 : 그 나머지는 컴파일러가 체크하고 예외를 던지던지 처리하던지 해야한다, 때문에 개발자는 모든 예외를 잡거니 던지거나 처리해야하기 때문에, 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야되고, 또 예외가 누수되면서 의존관계까지 생기는 문제점이 있다. ex) SQLException

예외처리 기본 규칙


예외처리는 2가지 기본 규칙이 있는데
1. 처리하거나 던져야한다.
2. 예외를 잡거나 던질때에는 그 예외의 자식들도 함께 처리된다. ex) Exception을 catch로 잡으면 그 하위 예외들도 모두 잡힌다.

  • 만약 예외를 계속 던지면 그 예외는 호출스택에 있는 메서드를 계속 전달되다가 main메서드까지 오고 main메서드에서도 처리 되지않으면 프로그램이 종료가 된다.

체크예외 활용


Repository에서 생긴 SQLException과 NetworkClient에서 생긴 ConnectException이 계속 계층을 타고 던져진다는 문제점이 있고, service계층에선 저런 예외를 처리할 방법을 모른다는것이다. 그렇다고 SQLException이랑 ConnectionException을 적기 귀찮아서 Exception을 던지게 되면 다른 체크 예외를 체크할 수 있는 기능이 무효화 되고, 중요한 체크 예외를 다 놓치게 된다. 그리고 지금은 JDBC라서 SQLException이 터지지만 JPA로 바꾼다고 하면 모든 SQLEXceptionJPAException으로 바꿔야하는 예외 의존관계가 생긴다.

언체크예외(런타임예외) 활용


시스템에서 발생한 예외는 거의 복구가 불가능하기 때문에 런타임예외를 사용하면 서비스나 컨트롤러가 이런 복구 불가능한 예외를 신경쓰지 않아서 되고, 이런 예외들은 일관성 있게 공통으로 처리한것이 좋다.
또한 해당 객체가 처리할 수 없는 예외는 무시하면 되기때문에 예외를 강제로 의존하지 않아도 된다는 장점이 있다.

체크예외와 인터페이스

서비스 계층은 가급적 특정 구현 기술에 의존하지 않고, 순수하게 유지하는 것이 좋다. 그러면 어떻게 SQLExeption에 대한 의존을 제거할까? 정답은 SQLException을 런타임 예외로 전환해서 서비스계층에 던지는 것이다. 그럼 서비스계층은 이 예외를 무시할수가 있다.

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

먼저 인터페이스를 적용해서 특정 DB기술에 종속되지 않게 하자.

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);
    }
    
}

그리고 SQLExeption을 런타임 예외로 전환하기위한 커스텀 예외를 만들자.

@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);
        }
    }
}

SQLException 부분을 MyDbException으로 바꿔서 다시 던져주었다.

   
   //변경전
    @Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
              bizLogic(fromId, toId, money);
    }
   //변경후
    @Transactional
    public void accountTransfer(String fromId, String toId, int money) {
              bizLogic(fromId, toId, money);
    }

그러면 throws 부분이 사라지면서 더이상 SQLException을 의존하지 않는걸 볼 수 있다.

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

데이터베이스 오류에 따라 특정 예외는 복구하고 싶을 수가 있다. ex) 회원 가입때 DB에 이미 같은 ID가 있으면 ID 뒤에 숫자를 붙여서 새로운 ID를 만드는 로직

DB에서 같은 ID가 이미 저장되어 있다면, 데이터베이스는 오류 코드를 반환하고 이 오류 코드는 SQLException에 다시 담겨서 던져진다. h2에서의 키중복오류코드는 23505이고, Mysql에서는 1062이다. 그리고 다른 수없이많은 에러코드가 있을텐데, 이걸 다 만들고 또 db마다 새로 만들어야될까?

스프링 예외 추상화


물론 스프링이 다 해결했다. 스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층을 제공한다. 크게 TransientNonTransient로 나누는데
Transient : 일시적이란 뜻으로, Transient의 하위 예외를 다시 시도했을때 다시 성공할 가능성이 있는 예외들을 말한다.
NonTransient : 일시적이지 않다는 뜻으로, 같은 SQL을 그대로 반복하면 실패하는 그런 예외들을 말한다.
스프링 예외 발생기의 사용법은 첫번째 파라미터에는 읽을수 있는설명, 두번째 파리미터에는 sql, 마지막은 발생된 SqlException을 전달해주면된다.


그리고 스프링은 다음과 같은 파일을 통해 각 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(?, ?)";
        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);
        }
    }
}    

SQLExeption을 스프링 예외 전환기에 전환시켜서 던져주는 모습을 확인할 수 있다.

0개의 댓글