스프링 DB 1편 - 데이터 접근 핵심 원리 : Repository 예외 처리, JdbcTemplate

jkky98·2024년 8월 22일
0

Spring

목록 보기
35/77

SQLException

	@Transactional
    public void accountTransfer(String fromId, String toId, int money) throws SQLException {
        bizLogic(fromId, toId, money);
    }

짧고 직관적인 우리의 비즈니스 로직 코드가 완성되었다. 하지만 여전히 이 코드는 Jdbc에 의존적이다. Jdbc 라이브러리로부터 온 SQLException때문이다. 정확히는 위 메서드는 SQLException를 던져주고 있다.(던져야만 한다) bizLogic에서 예외가 발생가능하며 bizLogic에는 리포지토리를 사용하고 있는데 리포지토리의 모든 데이터 접근 메서드에서 SQLException이 발생 가능하기 때문이다.

Jdbc는 옛날 기술이다. 예전의 자바 생태계는 라이브러리들이 예외의 크리티컬함을 구분하여 언체크, 체크 예외를 구분하여 raise했다. 목적은 크리티컬하니 개발자로 하여금 이를 잘 인지하고 처리하라는 것이다.

하지만 현재에 와서 이러한 예외를 조건로직을 통해 자바 코드로 자동적으로 처리할 확률은 극히 낮다. 업친데 덥친격으로 어플리케이션은 수 많은 라이브러리와 외부환경과 연결되어 발생 가능한 예외가 매우 많아졌다. 모든 발생가능한 체크 예외에 대해 throws를 통해 명시해야하고 이를 호출한 곳에 다시 또 다시. 유지보수성은 폭발적으로 악화된다. (이를 예외 누수라고 한다.)

어쨌든 과거의 얘기는 뒤로 하고, 라이브러리들은 초기에 설계한 체크 예외를 뱉는 방식으로 설계했던 것을 언체크(런타임)으로 바꿀 수는 없다. 레거시들이 망가질 것이기 때문에 그렇다. 그렇다면 이렇게 발생하는 체크 예외를 초기에 진압해야한다.

리포지토리를 넘어 서비스로, 서비스를 넘어 컨트롤러까지 throws SQLException을 붙일 수는 없기 때문이다. Jpa로 데이터 접근 기술을 바꾸기라도 한다면 SQLException을 모두 다른 예외로 바꿔주어야 한다.(Jpa가 뿜는 예외는 다르다.) 즉 리포지토리에서 발생가능한 체크 예외를 런타임 타입 이하의 예외로 바꾸어 던져야 한다.(트랜드)

이때동안 실습에서 Repository 클래스를 인터페이스 아래의 구현으로 사용하지 못한 이유또한 SQLException 때문이다. 인터페이스가 예외를 던지지 않는다면 자식도 던질 수 없기 때문이다. (인터페이스에서도 던져주면 되긴한다.)

ErrorCode(DB)

DB마다 에러의 상황 별로 주는 ErrorCode들이 존재한다. 스프링은 이를 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>

예로 H2데이터베이스의 42000 에러코드는 badSqlGrammerCodes이다. 즉 sql 문법이 틀렸다는 것이다.

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

    public Member save(Member member) {
        String sql = "insert into member(member_id, money) values (?, ?)";

        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = getConnection();
            // prepareStatement -> Statement와 달리 동적으로 쿼리문을 구성할 수 있음.
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            // 쿼리문 실제 DB에 적용
            pstmt.executeUpdate();
            return member;

        } catch (SQLException e) {
            throw exTranslator.translate("save", sql, e);
        } finally {

            close(con, pstmt, null);
        }

    }

체크 예외(SQLException)를 알아서 적절한 런타임 예외로 바꾸어 주기 위해 우리는 SQLExceptionTranslator의 translate메서드를 활용한다.

DataAccessException(sql 예외 계층)


변환되는 런타임 에러의 최상위는 DataAccessException이며 2가지(NonTransient(비일시적), Transient(일시적))로 구분된다. 일시적 예외의 경우 SQL을 다시 시도했을 때 성공할 가능성이 있는 경우이다.(락이 걸려있거나 쿼리 타임아웃의 경우에 해당한다)

이 중에서 각 DB마다의 에러코드와 적절한 런타임 예외를 매핑하여 예외 변환기가 return한다.

JdbcTemplate

// 템플릿 적용 전
public Member save(Member member) {
        String sql = "insert into member(member_id, money) values (?, ?)";

        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = getConnection();
            // prepareStatement -> Statement와 달리 동적으로 쿼리문을 구성할 수 있음.
            pstmt = con.prepareStatement(sql);
            pstmt.setString(1, member.getMemberId());
            pstmt.setInt(2, member.getMoney());
            // 쿼리문 실제 DB에 적용
            pstmt.executeUpdate();
            return member;

        } catch (SQLException e) {
            throw exTranslator.translate("save", sql, e);
        } finally {

            close(con, pstmt, null);
        }

    }
// 템플릿 적용 후
public Member save(Member member) {
        String sql = "insert into member(member_id, money) values (?, ?)";
        template.update(sql, member.getMemberId(), member.getMoney());
        return member;

    }

리포지토리의 CRUD기능을 보면 대부분 동일한 순서를 가지고 있다. con과 pstmt를 null로 초기화 한 후 커넥션을 가져오고 pstmt로 하여금 sql을 날려주고 예외가 발생하면 예외 변환기로 자동으로 매핑된 런타임 에러로 변환해서 던지고 결과에 상관없이 자원을 종료해주는 이 과정을 템플릿 콜백 패턴을 사용한JdbcTemplate로 하여금 깔끔하게 해결할 수 있다.

템플릿 콜백 패턴의 자세한 논리는 후에 다루도록 한다. 결국 우리가 Jdbc를 데이터 접근 기술로 사용한다면 우리는 JdbcTemplate를 사용하여 CRUD를 간단하게 구성해볼 수 있다.

트랜잭션의 유무를 구별하여 커넥션을 가져오고, 예외 발생 시 예외 변환기를 이용해 런타임 예외로 돌리는 등의 과정을 JdbcTemplate이 자동으로 처리해주는 것이다.

profile
자바집사의 거북이 수련법

0개의 댓글