4 분 소요


스프링 예외 추상화 이해

스프링은 앞서 설명한 문제들을 해결하기 위해 데이터 접근과 관련된 예외를 추상화해서 제공한다.

앞어 설명한 문제참고

스프링 데이터 접근 예외 계층
65
(일부 계층 생략..)

  • 스프링은 데이터 접근 계층에 대한 수십가지 예외를 정리해서 일관된 예외 계층을 제공한다.
  • 각각의 예외는 특정 기술에 종속적이지 않게 설계되어 있다. 따라서, 서비스 계층에서도 스프링이 제공하는 예외를 사용하면 된다.
  • JDBC나 JPA를 사용할 때 발생하는 예외를 스프링이 제공하는 예외로 변환해주는 역할도 스프링이 제공한다.

  • 예외의 최고 상위는 org.springframework.dao.DataAccessException이다.
    • 런타임 예외를 상속 받았기 때문에 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다.
  • DataAccessException은 크게 NonTransient예외와 Transient예외로 구분한다.
    • Transient는 일시적이라는 뜻으로, Transient 하위 예외는 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있다.
    • Ex) 쿼리 타임아웃, 락과 관련된 오류
      • 이런 오류들은 데이터베이스 상태가 좋아지거나, 락이 풀렸을 때 다시 시도하면 성공할 수도 있다.
    • NonTransient는 일시적이지 않다는 뜻으로, 같은 SQL을 그대로 반복해서 실행하면 실패한다.
      • Ex) SQL 문법 오류, 데이터베이스 제약조건 위배 등이 있다.

스프링이 제공하는 예외 변환기

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

먼저 에러 코드를 확인하는 코드를 간단히 확인.

SpringExceptionTranslatorTest

@Slf4j
public class SpringExceptionTranslatorTest {

    DataSource dataSource;

    @BeforeEach
    void init() {
        dataSource = new DriverManangerDataSource(URL, USERNAME, PASSWORD);
    }

    @Test
    void sqlExceptionErrorCode() {
        
        String sql = "select bad grammar";
        
        try {
            Connection con = dataSource.getConnection();
            PreparedStatement stmt = con.prepareStatement(sql);
            stmt.executeQuery();
        } catch (SQLException e) {
            assertThat(e.getErrorCode()).isEqualTo(42122);

            int errorCode = e.getErrorCode();
            log.info("errorCode={}", errorCode);
            //org.h2.jdbc.JdbcSQLSyntaxErrorException
            log.info("error", e);
        }
    }
}
  • SQL ErrorCode를 직접 확인하며 스프링이 만들어준 예외로 변환하는 것은 현실성이 없다.
  • 그래서 스프링은 예외 변환기를 제공한다.

SpringExceptionTranslatorTest - 추가 exceptionTranslator

@Test
void exceptionTranslator() {

    String sql = "select bad grammar";

    try{
        Connection con = dataSource.getConnection();
        PreparedStatement stmt = con.prepareStatement(sql);
        stmt.executeQuery();
    } catch(SQLException e) {
        assertThat(e.getErrorCode()).isEqualTo(42122);

        SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
        DataAccessException resultEx = exTranslator.translate("select", sql, e);
        log.info("resultEx", resultEx);

        assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
    }
}
  • translate()메서드의 첫 번째 파라미터는 읽을 수 있는 설명이고, 두 번째는 실행한 SQL, 마지막은 발생된 SQLException을 전달하면 된다.
  • 예제에서는 SQL 문법이 잘못되었으므로 BadSqlGrammarException을 반환하는 것을 확인할 수 있다.
    • 반환 타입은 최상위 타입인 DataAccessException이지만 실제로는 BadSqlGrammarException 예외가 반환된다.

각각의 DB마다 SQL ErrorCode는 다른데, 스프링은 어떻게 각각의 DB가 제공하는 SQL ErrorCode까지 고려해서 예외를 변환할 수 있을까?

sql-error-codes.xml

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

...
  • org.springframework.jdbc.support.sql-error-codes.xml
  • 스프링 SQL 예외 변환기는 SQL ErrorCode를 이 파일에 대입해서 어떤 스프링 데이터 접근 예외로 전환해야 할지 찾아낸다.

정리

  • 스프링은 데이터 접근 계층에 대한 일관된 예외 추상화를 제공한다.
  • 스프링은 예외 변환기를 통해서 SQLExceptionErrorCode에 맞는 적절한 스프링 데이터 접근 예외로 변환해준다.
  • 만약 서비스, 컨트롤러 계층에서 예외 처리가 필요하면 특정 기술에 종속적인 SQLException같은 예외를 직접 사용하는 것이 아니라, 스프링이 제공하는 데이터 접근 예외를 사용하면 된다.
  • 물론 스프링이 제공하는 예외를 사용하기 때문에 스프링에 대한 기술 종속성은 발생한다.
    • 스프링에 대한 기술 종속성까지 완전히 제거하려면 예외를 모두 직접 정의하고, 예외 변환도 직접 하면 되지만 실용적인 방법은 아니다.


스프링 예외 추상화 적용

MemberRepositoryV4_2

/**
 * SQLExceptionTranslator 추가
 */
@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);
        }
    }

    ...
}
  • 기존 코드에서 스프링 예외 변환기를 사용하도록 변경되었다.

MemberServiceV4Test - 수정

@Bean
MemberRepository memberRepository() {
    // return new MemberRepositoryV4_1(dataSource); // 단순 예외 반환
    return new MemberRepositoryV4_2(dataSource); // 스프링 예외 반환
}
  • MemberRepository 인터페이스가 제공되므로 스프링 빈에 등록할 빈만 교체하면 리포지토리를 변경해서 테스트를 확인할 수 있다.

정리

스프링이 예외를 추상화 해준 덕분에 서비스 계층은 특정 리포지토리의 구현 기술과 예외에 종속적이지 않게 되었다. 따라서, 서비스 계층은 특정 구현 기술이 변경되어도 그대로 유지할 수 있게 되었다.
추가로, 서비스 계층에서 예외를 잡아서 복구해야 하는 경우, 예외가 스프링이 제공하는 데이터 접근 예외로 변경되어서 서비스 계층에 넘어오기 때문에 필요한 경우 예외를 잡아서 복구하면 된다.


JdbcTemplate

리포지토리에서 JDBC를 사용하기 때문에 발생하는 반복 문제를 해결.

JDBC 반복 문제

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

이런 반복을 효과적으로 처리하는 방법이 템플릿 콜백 패턴이다.
스프링은 JDBC의 반복 문제를 해결하기 위해 JdbcTemplate이라는 템플릿을 제공한다.
지금은 전체 구조와 이 기능을 사용함으로써 반복 코드를 제거할 수 있다는 것에 초점을 맞추면 된다.

MemberRepositoryV5

/**
 * JdbcTemplate 사용
 */
@Slf4j
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;
    }

    @Override
    public Member findById(String memberId) {
        String sql = "select * from member where member_id = ?";
        return template.queryForObject(sql, memberRowMapper(), memberId);
    }

    private RowMapper<Member> memberRowMapper() {
        return (rs, rowNum) -> {
            Member member = new Member();
            member.setMemberId(rs.getString("member_id"));
            member.setMoney(rs.getInt("money"));
            return member;
        };
    }

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

    @Override
    public void delete(String memberId) {
        String sql = "delete from member where member_id=?"
        template.update(sql, memberId);
    }
}

JdbcTemplate은 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다. 그 뿐만 아니라 트랜잭션을 위한 커넥션 동기화는 물론이고, 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다.


정리

  • 서비스 계층의 순수성
    • 트랜잭션 추상화 + 트랜잭션 AOP 덕분에 서비스 계층의 순수성을 유지하면서 서비스 계층에서 트랜잭션을 사용할 수 있다.
    • 스프링이 제공하는 예외 추상화와 예외 변환기 덕분에 데이터 접근 기술이 변경되어도 서비스 계층의 순수성을 유지하면서도 예외도 사용할 수 있다.
    • 서비스 계층이 리포지토리 인터페이스에 의존한 덕분에 향후 리포지토리가 다른 구현 기술로 변경되어도 서비스 계층을 순수하게 유지할 수 있다.
  • 리포지토리에서 JDBC를 사용하는 반복 코드가 JdbcTemplate으로 대부분 제거되었다.


<출처 : 인프런 - 스프링 DB 2편 : 데이터 접근 활용 기술(김영한)>

댓글남기기