minghxx.blog
  • [Spring] 스프링 DB 1편 6) 스프링과 문제 해결 - 예외 처리, 반복
    2023년 11월 21일 09시 42분 06초에 업로드 된 글입니다.
    작성자: 민발자
    728x90

    스프링 DB 1편 데이터 접근 핵심 원리

    Session 6 스프링과 문제 해결 - 예외 처리, 반복

    1. 체크 예외와 인터페이스

    1) 체크 예외와 인터페이스

    서비스 계층은 특정 구현 기술에 의존하지 않고 순수하게 유지하는게 좋음 예외에 대한 의존도 해결해야 함

    리포지토리가 SQLException을 런타임 예외로 전환해 서비스 계층에 던지면 서비스 계층을 해당 예외를 무시할 수 있어 특정 구현 기술에 의존하는 부분을 제거하고 서비스 계층을 순수하게 유지할 수 있다.

     

    2) 인터페이스 도입

    MemberService는 MemberRepository 인터페이스만 의존, 구현 기술을 변경하고 싶으면 DI를 사용해 MemberService 코드 변경 없이 구현 기술만 변경할 수 있다.

     

    3) 체크 예외에 인터페이스 도입 시 문제점

    인터페이스의 구현체가 체크 예외를 던지려면 인터페이스 메서드에 먼저 체크 예외를 던지는 부분이 선언되어 있어야 구현 클래스의 메서드에서도 던질 수 있다.

    구현 클래스의 메서드에서 선언할 수 있는 예외는 부모 타입에서 던진 예외와 같거나 하위 타입이어야 한다.

    - MemberRepositoryEx

    SQLException이 체크 예외이기 때문에 인터페이스에도 선언이 필요

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

     

    4) 특정 기술에 종속되는 인터페이스

    인터페이스를 도입하더라도 SQLException과 같은 특정 구현 기술에 종속적인 체크 예외를 사용하게 되면 인터페이스에도 해당 예외를 포함해야 한다.

    런타임 예외가 이런 부분을 해결해 줌 ▶ 인터페이스에 런타임 예외를 따로 선언할 필요가 없으므로 인터페이스가 특정 기술에 종속적일 필요가 없다.

     


    2. 런타임 예외 적용

    1) 런타임 예외와 인터페이스

    - MemberRepository 인터페이스

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

     

    - MyDbException 런타임 예외

    RuntimeException을 상속받으면 런타임예외가 된다.

     

    2) 예외 변환 

    - MemberRepositoryV4_1

    체크 예외 선언 부분 제거, catch에서 MyDbException 런타임 예외로 변환해서 던짐, 기존 예외를 꼭 같이 포함해서 던지기!!

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

     

    3) 서비스 계층 인터페이스 도입

    - MemberServiceV4

    MemberRepository에 의존, throws SQLException 체크 예외 선언 부분 제거

    이제 서비스 계층은 특정 구현 기술에 의존하지 않고 순수하게 유지

     

    4) 정리

    체크 예외를 런타임 예외로 변경하면서 인터페이스와 서비스 계층의 순수성을 유지

    JDBC에서 다른 구현 기술로 변경하더라도 서비스 계층의 코드를 변경하지 않아도 된다.

    ▶ 리포지토리에서 넘어오는 특정한 예외의 경우 복구를 시도할 수 있는데 지금 방식은 MyDbException 예외만 넘어오기 때문에 예외를 구분할 수 없다. 특정 상황에서 예외를 구분해서 처리하기 위한 방법 필요


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

    1) 특정 예외 복구 예시

    데이터베이스 오류에 따라 특정 예외를 복구하고자 한다.

    예를 들어 회원가입 시 DB에 이미 같은 ID가 있으면 ID 뒤에 숫자를 붙여서 새로운 ID를 생성하고자 한다.

    ID가 이미 데이터베이스에 저장되어 있다면 데이터베이스는 오류 코드를 반환하고 이 오류 코드를 받은 JDBC 드라이버는 SQLException을 던진다. SQLException 내부에는 데이터베이스가 제공하는 errorCode를 포함하고 있다.

    리포지토리는 SQLException을 서비스 계층에 던지고 서비스 계층은 예외의 오류 코드를 확인해 키 중복 오류(23505)인 경우 새로운 ID를 만들어 다시 저장을 시도

    이렇게 되면 다시 서비스 계층이 JDBC 기술에 의존하게 되면서 순수성이 무너지기 때문에 리포지토리에서 SQLException을 MyDuplicateException 예외로변환해 던지자!

     

    2) H2 데이터베이스 오류 코드

    같은 오류여도 데이터베이스마다 정의된 오류 코드가 다르다. 사용하고 있는 데이터베이스 매뉴얼 확인!!

    e.getErrorCode() == 23505
    
    // 23505 : 키 중복 오류
    // 42000 : SQL 문법 오류

     

    3) MyDuplicateException 예외 

    데이터 중복일 경우에만 사용하는 예외

    기존에 사용했던 MyDbException을 상속받아 의미 있는 계층 형성(데이터베이스 관련 예외 계층)

    직접 만든 예외이기 때문에 JDBC나 JPA 같은 특정 기술에 종속적이지 않다. 이 예외를 사용하더라고 서비스 계층의 순수성을 유지 가능

     

    4) 테스트

    - 테스트 결과

    Service - saveId=myId
    Service - 키 중복, 복구 시도
    Service - retryId=myId492

    같은 ID를 저장했지만 중간에 예외를 잡아 복구 진행

     

    - 리포지토리

    catch (SQLException e) {
      //h2 db
      if (e.getErrorCode() == 23505) {
          throw new MyDuplicateKeyException(e);
      }
      throw new MyDbException(e);
    }

    오류 코드가 키 중복 오류인 경우 MyDuplicateKeyException 예외를 서비스 계층에 던진다.

    나머지 경우 MyDbException을 던짐

     

    - 서비스

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

    저장 시도 MyDuplicateKeyException가 올라오면 예외를 잡아 새로운 아이디를 생성 후 다시 저장 시도(예외 복구)

    복구할 수 없는 예외면(MyDbException) 로그를 남기고 던진다 ▶ 복구할 수 없는 예외는 공통 처리 부분에서 처리, 예외 로그도 공통 처리 부분에서 남기는 것이 좋으나 예시로 서비스에서 로그를 남겼다.

     

    5) 정리

    SQL errorCode로 데이터베이스에 어떤 오류가 있는지 확인할 수 있음

    예외 변환을 통해 SQLException을 특정 기술에 의존하지 않고 직접 만든 예외인 MyDuplicateKeyException로 변환할 수 있었다.

    리포지토리 계층이 예외를 변환해 준 덕분에 서비스 계층은 특정 기술에 의존하지 않는 MyDuplicateKeyException을 사용해 문제를 복구하고 서비스 계층의 순수성도 유지할 수 있다

    ▶ SQL errorCode는 데이터베이스마다 다르기 때문에 데이터베이스 구현 기술을 변경할 때마다 코드 수정이 필요하다. 스프링 예외 추상화 사용!!


    4. 스프링 예외 추상화 이해

    1) 스프링 데이터 접근 예외 계층

    스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층을 제공

    각가의 예외는 특정 기술에 종속적이지 않게 설계되어 있어 서비스 계층에서도 스프링이 제공하는 예외를 사용

    JDBC나 JPA를 사용할 때 발생하는 예외를 스프링이 제공하는 예외로 변환해 주는 역할도 스프링이 제공

    런타임 예외를 상속받았기 때문에 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외

    DataAccessException은 NonTransient, Transient 예외 2가지로 구분

    → Transient : 일시적이라는 뜻, Transient 하위 예외는 동일한 SQL을 다시 시도했을 때 성공 가능성이 있다

    퀴리 타임아웃, 락과 관련된 오류로 데이터베이스 상태가 좋아지거나 락이 풀렸을 때 다시 시도하면 성공 가능

     NonTransient : 일시적이지 않다는 뜻, 같은 SQL을 그대로 반복해서 실행하면 실패한다.

    SQL 문법 오류, 데이터베이스 제약 조건 위배 등

     

    2) 스프링 제공 예외 변환기

    스프링은 데이터베이스에서 발생한 오류코드를 스프링이 정의한 예외로 자동 변환해 주는 변환기 제공

    각 DB마다 SQL ErrorCode가 다른데 어떻게 스프링은 예외를 변환? sql-error-codes.xml DB의 ErrorCode마다 어떤 예외를 반환할지 정의되어 있음

    SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    
    //org.springframework.jdbc.BadSqlGrammarException
    DataAccessException resultEx = exTranslator.translate("select", sql, e);

    translate("설명", 실행한 sql, 발생된 예외) → 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환해 준다.

    현재 sql 문법이 잘못되었기 때문에 BadSqlGrammarException 반환

     

    3) 정리

    스프링은 데이터 접근 계층에 대한 일관된 예외 추상화 제공

    예외 변환기를 통해 SQLException의 ErrorCode에 맞는 적절한 스프링 데이터 접근 예외로 변환

    스프링 예외 추상화 덕분에 특정 기술에 종속적이지 않고 DB 기술이 변경되어도 코드 수정이 불필요

    스프링이 제공하는 예외를 사용하기 때문에 스프링에 대한 기술 종속성을 발생 스프링 기술 종속성까지 제거하기 위해선 직접 예외 정의 변환하면 되지만 실용성 떨어짐


    5. 스프링 예외 추상화 적용

    1) 정리

    스프링의 예외 추상화를 사용해서 서비스 계층은 특정 리포지토리의 구현 기술과 예외에 종속적이지 않게 되었다.

    서비스 계층은 특정 구현 기술이 변경되어도 그대로 유지할 수 있게 되었고 DI를 제대로 활용할 수 있게 됨

    추가로 서비스 계층에서 예외를 잡아서 복구해야 하는 경우 예외가 스프링이 제공하는 데이터 접근 예외로 변경되어서 서비스 계층에 넘어오기 때문에 필요한 경우 예외를 잡아서 복구하면 된다.


    6. JDBC 반복 문제 해결 - JdbcTemplate 

    1) JDBC 반복 문제

    커넥션 조회, 커넥션 동기화, PreparedStatement 생성 및 파라미터 바인딩, 쿼리 실행, 결과 바인딩, 예외 발생 시 스프링 예외 변환기 실행, 리소스 종료

    리포지토리의 각각 메서드를 살펴보면 위와 같은 코드들이 반복된다. 반복을 효과적으로 처리하는 방법이 템플릿 콜백 패턴

     

    2) JdbcTemplate

    public class MemberRepositoryV5 implements MemberRepository {
          private final JdbcTemplate template;
          
          public MemberRepositoryV5(DataSource dataSource) {
              template = new JdbcTemplate(dataSource);
          }
          
          @Override
          public Member save(Member member) {
              String sql = "insert into member(member_id, money) values(?, ?)";
              template.update(sql, member.getMemberId(), member.getMoney());
              return member;
          }
          
          ...
    }

    JdbcTemplate으로 트랜잭션을 위한 커넥션 동기화, 스프링 예외 변환기도 자동으로 실행

     

    3) 정리

    → 서비스 계층의 순수성

    트랜잭션 추상화 + 트랜잭션 AOP 덕분에 서비스 계층의 순수성을 최대한 유지하면서 트랜잭션을 사용

    스프링이 제공하는 예외 추상화와 예외 변환기 덕분에 데이터 접근 기술이 변경되어도 서비스 계층의 순수성을 유지하면서 예외도 사용가능

    서비스 계층이 리포지토리 인터페이스에 의존한 덕분에 향후 리포지토리가 다른 구현 기술로 변경되어도 서비스 계층 순수하게 유지 가능

    → 리포지토리 반복 코드 제거

    JdbcTemplate을 사용해 반복 코드 대부분 제거

     

     

     

     

     

     

     

     

    728x90
    댓글