김영한 님의 스프링 DB 1편 - 데이터 접근 핵심 원리 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-db-1/dashboard


1. 문제점

1-1. Service의 문제점

public class MemberServiceV3_3 {

    private final MemberRepositoryV3 memberRepository;
    
    private void bizLogic(String fromId, String toId, int money) throws SQLException { ... }
    ...
}
  • MemberRepositoryV3SQLException 에 의존하는 문제

  • ➜ 순수한 Service를 위해 MemberRepository 인터페이스를 만든다


1-2. Repository의 문제점

public class MemberRepositoryV3 {

    public Member save(Member member) throws SQLException { ... }
    ...
}
  • Repository의 메서드는 SQLException 이라는 체크 예외를 던지고 있는데 인터페이스의 구현체가 체크 예외를 던지려면 인터페이스의 메서드가 체크 예외를 던지도록 선언되어 있어야한다

  • but> 인터페이스의 메서드에 throws 로 예외를 던지는 부분이 선언되어 있으면 이는 특정 기술에 의존적인 인터페이스

  • ➜ 특정 기술에 의존하지 않도록 구현체는 체크 예외를 런타임 예외로 전환해서 서비스 계층에 던져야 한다




2. 런타임 예외 적용

2-1. Repository

public class MemberRepositoryV4_1 implements MemberRepository{

    @Override
    public Member save(Member member) {

        try {
            ...
        } catch (SQLException e) {
            throw new MyDbException(e);
        } finally {
            close(con, pstmt, null);
        }
    }
}
  • MemberRepository 인터페이스를 구현

  • 메서드에 선언되어 있던 throws SQLException 제거

  • catch에서 체크 예외를 잡아 RuntimeException을 상속받은 MyDbException을 반환

  • ➡️체크 예외를 런타임 예외로 변환하면서 예외 의존 제거


2-2. Service

public class MemberServiceV4 {

    private final MemberRepository memberRepository;

    private void bizLogic(String fromId, String toId, int money) { ... }
    ...
}
  • MemberRepository 인터페이스에 의존

  • Repository의 메서드를 사용하는 bizLogic()에서 SQLException이 제거됨

    • Repository의 메서드에서 체크 예외를 런타임 예외로 변경했기 때문
  • ➡️특정 구현체 의존 제거 및 예외 의존 제거


2-3. 정리

  • 예외에 의존하는 것을 제거하기 위해 체크 예외를 런타임 예외로 변환하면서 인터페이스와 서비스 계층의 순수성을 유지할 수 있게 되었다

  • 서비스가 인터페이스에 의존하도록 변경하여 데이터 접근 기술을 변경해도 서비스의 코드는 변경하지 않는다




3. 데이터 접근 예외 및 복구

3-1. DB 에러 코드

  1. Repository가 JDBC 드라이버에 insert 쿼리 전달 ➜ JDBC 드라이버가 DB에 쿼리 전달

  2. 상황에 따라 DB 내부에 있는 오류 코드를 반환

  3. JDBC 드라이버가 SQLException을 만드는데 내부에 오류 코드를 넣어놓는다

  4. Repository로 예외가 넘어오면 SQLException 내부의 오류 코드를 확인할 수 있다

    • e.getErrorCode()로 오류 코드를 확인할 수 있다

    • SQLException 내부의 errorCode를 활용하면 DB에 발생한 문제를 알 수 있다

    • 오류 코드는 DB 마다 다르며, 같은 오류 코드여도 DB 마다 다르다


3-2. 예외 복구 흐름

  • 특정 ID로 가입을 시도 했는데, 이미 같은 아이디가 있으면 뒤에 임의의 숫자를 붙여서 가입한다고 가정했을 때 이러한 흐름이 예외를 확인해서 복구하는 과정이다

  • 오류 복구를 위해 서비스 계층에서는 예외 복구를 위해 키 중복 오류를 확인할 수 있어야한다

  • but> SQLException 내부의 오류 코드를 활용하기 위해 예외를 서비스로 던지게 되면 서비스가 JDBC 기술에 의존하게 된다

  • ➡️Repository에서 예외를 변환해서 서비스로 던진다


3-3. 복구를 위한 예외 생성

  • 키 중복일 때 발생시킬 예외를 MyDuplicateKeyException 을 생성한다

    • ➜ 직접 만든 예외이기 때문에 특정 데이터 접근 기술에 종속적이지 않다

    • ➜ 즉, 서비스 계층의 순수성을 유지할 수 있다

  • MyDuplicateKeyException 은 이전에 만들었던 MyDbException 을 상속받는다

    • ➜ DB 관련 예외라는 계층을 만들 수 있다

3-4. 예외 복구 코드 작성

3-4-1. Repository

static class Repository {

    public Member save(Member member) {
        try {
            ...
        } catch (SQLException e) {
            if (e.getErrorCode() == 23505) {
                throw new MyDuplicateKeyException(e);
            }
            throw new MyDbException(e);
        } finally {
            ...
        }
    }
}
  • 예외 코드가 23505라면 MyDuplicateKeyException 을 새로 만들어서 서비스에 던진다

  • 그 외의 예외는 MyDbException 을 만들어서 던진다


3-4-2. Service

static class Service {

    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 + new Random().nextInt(10000);
    }
}
  • 저장을 시도했는데 MyDuplicateKeyException 예외가 올라오면 이 예외를 잡아서 generateNewId() 로 새로운 ID 생성을 시도

  • 그리고 다시 저장하는 메서드를 호출하는데 여기가 예외를 복구하는 부분이다

  • 복구할 수 없는 예외( MyDbException )면 로그만 남기고 다시 예외를 던진다

    • 복구할 수 없는 예외는 예외를 공통으로 처리하는 부분까지 전달되기 때문에 굳이 로그를 남길 필요는 없다



4. 스프링 예외 추상화

4-1. 필요성

  • 위에서 에러 코드에 따른 예외 복구 코드를 작성했는데 SQL ErrorCode는 각각의 데이터베이스 마다 다르다

  • 또한 DB에 정의된 오류 코드는 중복 키 오류를 제외하고도 많은 오류 코드가 존재한다

  • DB가 변경될 때마다 코드를 변경하는 것은 현실성이 없다

  • 또한 모든 오류 코드를 확인해서 직접 예외를 변환시키는 것도 불가능

  • ➡️스프링은 이런 문제들을 해결하기 위해 데이터 접근과 관련된 예외를 추상화해서 제공


4-2. 스프링 데이터 접근 예외 계층

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

  • 각 예외는 특정 기술에 종속되지 않기 때문에 서비스 계층에서 스프링이 제공하는 예외를 사용하면 된다

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


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

  • 예외의 최고 상위는 DataAccessExcepton이고 TransientNonTransient 로 나뉜다

  • Transient : 일시적이라는 의미로 이 하위 예외들은 동일한 SQL을 다시 시도하면 성공할 가능성이 있다

  • NonTransient : 동일한 SQL을 다시 시도해도 실패한다


4-3. 스프링이 제공하는 예외 변환기

SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
  • 스프링은 DB에서 발생하는 오류 코드를 스프링이 정의한 예외로 자동으로 변환해주는 변환기를 제공한다

  • translate() 메서드의 첫번째 파라미터는 읽을 수 있는 설명이고, 두번째는 실행한 sql, 마지막은 발생된 SQLException 을 전달하면 된다

  • 이렇게 하면 SQL을 분석해 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환한다

  • ➜ 서비스, 컨트롤러에서 예외 처리가 필요하면 특정 기술에 종속적인 SQLException 같은 예외를 직접 사용하는 것이 아니라, 스프링이 제공하는 데이터 접근 예외를 사용


4-4. 예외 변환기 작동 원리

<bean id="H2" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="badSqlGrammarCodes">
        <value>42000,42001,42101,42102,42111,42112,42121,42122,42132</value>
    </property>
    <property name="duplicateKeyCodes">
        <value>23001,23505</value>
    </property>
</bean>

<bean id="MySQL" class="org.springframework.jdbc.support.SQLErrorCodes">
    <property name="badSqlGrammarCodes">
        <value>1054,1064,1146</value>
    </property>
    <property name="duplicateKeyCodes">
        <value>1062</value>
    </property>
</bean>
  • DB마다 SQL ErrorCode가 다른데 스프링이 각각의 DB가 제공하는 ErrorCode까지 고려해서 예외를 변환할 수 있는 이유는 sql-error-codes.xml

  • org.springframework.jdbc.support.sql-error-codes.xml

  • 스프링 SQL 예외 변환기는 SQL ErrorCode를 위의 파일에 대입해서 어떤 스프링 데이터 접근 예외로 전환해야 할지 찾아낸다

  • ex> H2 데이터베이스에서 42000 이 발생하면 badSqlGrammarCodes 이기 때문에 BadSqlGrammarException 을 반환




5. 예외 변환기 적용하기

public class MemberRepositoryV4_2 implements MemberRepository{

    private final DataSource dataSource;
    private final SQLExceptionTranslator exceptionTranslator;

    public MemberRepositoryV4_2(DataSource dataSource) {
        this.dataSource = dataSource;
        this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
    }


    @Override
    public Member save(Member member) {
        ... 
        catch (SQLException e) {
            throw exTranslator.translate("save", sql, e);
        } 
        ...
    }
}
  • SQLExceptionTranslator : 인터페이스

  • SQLErrorCodeSqlExceptionTranslator : 구현체

    • 어떤 DB를 사용하는지 등의 정보가 필요하기 때문에 dataSource를 파라미터로 전달
  • translate() 가 예외를 반환하기 때문에 바로 throw 를 통해 예외 발생 가능




6. JdbcTemplate

6-1. 설명

  • Repository 에서 JDBC 를 사용하면서 아래 과정( 코드 )들이 계속 반복

    • 커넥션 조회, 커넥션 동기화

    • PreparedStatement 생성 및 파라미터 바인딩

    • 쿼리 실행, 결과 바인딩

    • 예외 발생 시, 스프링 예외 변환기 실행

    • 리소스 종료

  • ➡️템플릿 콜백 패턴을 이용해 반복을 처리할 수 있다

  • ➡️스프링은 JDBC 반복 문제 해결을 위해 JdbcTemplate 을 제공


6-2. 코드에 적용

public class MemberRepositoryV5 implements MemberRepository{

    private final JdbcTemplate template;

    public MemberRepositoryV5(DataSource dataSource) {
        this.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;
    }


    @Override
    public Member findById(String memberId) {

        String sql = "select * from member where member_id = ?";
        return template.queryForObject(sql, memberRowMaper(), memberId);
    }


    private RowMapper<Member> memberRowMaper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setMemberId(rs.getString("member_id"));
            member.setMoney(rs.getInt("money"));
            return member;
        }
    };
}
  • 위처럼 작성하면 기존에 try, catch 내부에서 수행하던 모든 것을 자동으로 해준다

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

  • update()

    • sql 문과 ? 에 들어갈 파라미터 정보를 넘겨준다

    • 영향 받은 행 수를 반환

  • queryObject()

    • 한 건을 조회할때 사용

    • 쿼리 결과로 어떻게 객체를 만들 것인지에 대한 매핑 정보를 파라미터로 추가

    • memberRowMaper()가 매핑 정보, ResultSet을 활용

    • 마지막 파라미터는 ? 에 들어갈 정보

0개의 댓글

Powered by GraphCDN, the GraphQL CDN