Java Category/Spring

[Spring DB] 예외 추상화, jdbcTemplate

ReBugs 2024. 3. 5.

이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.


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

스프링은 데이터 접근과 관련된 예외를 추상화해서 제공한다.

그림을 단순화 하기 위해 일부 계층을 생략

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

 

각각의 예외는 특정 기술에 종속적이지 않게 설계되어 있다. 따라서 서비스 계층에서도 스프링이 제공하는 예외를 사용하면 된다. 예를 들어서 JDBC 기술을 사용하든, JPA 기술을 사용하든 스프링이 제공하는 예외를 사용하면 된다.

 

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

 

예외의 최고 상위는 org.springframework.dao.DataAccessException 이다. 그림에서 보는 것 처럼 런타임 예외를 상속 받았기 때문에 스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다.

 

DataAccessException 은 크게 2가지로 구분하는데 NonTransient 예외와 Transient 예외이다.

  • Transient 는 일시적이라는 뜻이다. Transient 하위 예외는 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있다. 예를 들어서 쿼리 타임아웃, 락과 관련된 오류들이다. 이런 오류들은 데이터베이스 상태가 좋아지거나, 락이 풀렸을 때 다시 시도하면 성공할 수도 있다.
  • NonTransient 는 일시적이지 않다는 뜻이다. 같은 SQL을 그대로 반복해서 실행하면 실패한다. SQL 문법 오류, 데이터베이스 제약조건 위배 등이 있다.
스프링 메뉴얼에 모든 예외가 정리되어 있지는 않기 때문에 코드를 직접 열어서 확인해보는 것이 필요하다.

 

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

@Test
void exceptionTranslator() {
    String sql = "select bad grammar";
    try {
        Connection con = dataSource.getConnection();
        PreparedStatement stmt = con.prepareStatement(sql);
        stmt.executeQuery();
    } catch (SQLException e) {
        //org.springframework.jdbc.support.sql-error-codes.xml
        SQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
        //org.springframework.jdbc.BadSqlGrammarException
        DataAccessException resultEx = exTranslator.translate("select", sql, e);
        log.info("resultEx", resultEx);
        assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
    }
}

translate() 메서드의 첫번째 파라미터는 읽을 수 있는 설명이고(개발자가 알아보기 쉽게 설정), 두번째는 실행한 sql, 마지막은 발생된 SQLException 을 전달하면 된다. 이렇게 하면 적절한 스프링 데이터 접근 계층의 예외로 변환해서 반환해준다.

 

예제에서는 SQL 문법이 잘못되었으므로 BadSqlGrammarException 을 반환하는 것을 확인할 수 있다.
눈에 보이는 반환 타입은 최상위 타입인 DataAccessException 이지만 실제로는 BadSqlGrammarException 예외가 반환된다. 마지막에 assertThat() 부분을 확인하자.

참고로 BadSqlGrammarException 은 최상위 타입인 DataAccessException 를 상속 받아서 만들어진다.

 

스프링은 예외 변환기를 통해서 SQLException 의 ErrorCode 에 맞는 적절한 스프링 데이터 접근 예외로 변환해준다.
만약 서비스, 컨트롤러 계층에서 예외 처리가 필요하면 특정 기술에 종속적인 SQLException 같은 예외를 직접 사용하는 것이 아니라, 스프링이 제공하는 데이터 접근 예외를 사용하면 된다.

 

스프링 예외 추상화 덕분에 특정 기술에 종속적이지 않게 되었다.

 

JDBC에서 JPA같은 기술로 변경되어도 예외로 인한 변경을 최소화 할 수 있다. 향후 JDBC에서 JPA로 구현 기술을 변경하더라도, 스프링은 JPA 예외를 적절한 스프링 데이터 접근 예외로 변환해준다.

 

물론 스프링이 제공하는 예외를 사용하기 때문에 스프링에 대한 기술 종속성은 발생한다.

스프링에 대한 기술 종속성까지 완전히 제거하려면 예외를 모두 직접 정의하고 예외 변환도 직접 하면 되지만, 실용적인 방법은 아니다.

 

스프링 예외 추상화 적용

public class MemberRepositoryV4_2 implements MemberRepository{
    private final DataSource dataSource;
    private final SQLExceptionTranslator exTranslator;
    public MemberRepositoryV4_2(DataSource dataSource) {
        this.dataSource = dataSource;
        //SQLExceptionTranslator 인터페이스에 여러가지가 있지만 SQLErrorCodeSQLExceptionTranslator 사용
        this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource); 
    }
    
    @Override
    public Member save(Member member) {
        String sql = "insert into member(member_id, money) values(?, ?)";
        try {
            //데이터베이스 save(저장) 로직
        } catch (SQLException e) {
            throw exTranslator.translate("save", sql, e); //예외를 던짐
        } finally {
            close(con, pstmt, null);
        }
    }
    
    @Override
    public Member findById(String memberId) {
        String sql = "select * from member where member_id = ?";
        try {
            //데이터베이스에서 특정 데이터를 찾는 로직
        } catch (SQLException e) {
            throw exTranslator.translate("findById", sql, e); //예외를 던짐
        } finally {
            close(con, pstmt, rs);
        }
    }
    ...
}

스프링이 예외를 추상화해준 덕분에, 서비스 계층은 특정 리포지토리의 구현 기술과 예외에 종속적이지 않게 할 수 있다.

따라서 서비스 계층은 특정 구현 기술이 변경되어도 그대로 유지할 수 있다.  DI를 제대로 활용할 수 있게 된 것이다.

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

 

jdbcTemplate(JDBC 반복문제 해결)

JDBC 반복 문제

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

 

데이터 접근 계층의 각각의 메서드에 jdbc 기술이 사용되면 상당히 많은 부분이 반복된다. 이런 반복을 효과적으로 처리하는 방법이 바로 템플릿 콜백 패턴이다.


스프링은 JDBC의 반복 문제를 해결하기 위해 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());
        //template.update()는 변경된 row 수를 리턴한다.
        return member;
    }

    @Override
    public Member findById(String memberId) {
        String sql = "select * from member where member_id = ?";
        return template.queryForObject(sql, memberRowMapper(), memberId); //주의
        //select 문으로 객체를 반환할 때, 콜백 패턴으로 따로 처리를 해줘야 한다.
    }

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

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

 

template.queryForObject(sql, memberRowMapper(), memberId);

이 코드는 스프링 프레임워크의 JdbcTemplate 클래스의 인스턴스인 template을 사용하여 데이터베이스에서 단일 객체를 조회하는 것이다. 이 구문에서 사용되는 메서드와 매개변수의 역할은 다음과 같다.

  • template은 JdbcTemplate의 인스턴스이다. JdbcTemplate은 스프링이 제공하는 클래스로, JDBC를 통해 데이터베이스에 접근하여 작업을 수행할 때 반복되는 코드와 예외 처리를 간소화하는 역할을 한다. 
  • queryForObject 메서드는 SQL 쿼리를 실행하여 결과로 반환되는 단일 객체를 조회하는 메서드이다. 이 메서드는 쿼리 결과가 정확히 하나의 객체만을 반환해야 한다. 반환되는 객체의 수가 하나가 아닌 경우 IncorrectResultSizeDataAccessException 예외가 발생한다.
  • sql은 데이터베이스에서 실행할 SQL 쿼리 문자열이다.
  • memberRowMapper()는 결과 행을 객체로 매핑하는 역할을 하는 RowMapper 인터페이스의 구현체를 반환하는 메서드이다. RowMapper는 SQL 쿼리의 결과로 얻어진 ResultSet의 각 행을 객체로 변환하는 방법을 정의한다.
  • memberId는 SQL 쿼리에서 사용할 매개변수이다. 이 경우, 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;
    };
}

위 코드는 데이터베이스로부터 Member 객체를 조회하기 위한 RowMapper<Member> 구현체를 제공하는 메서드이다. RowMapper 인터페이스는 JDBC ResultSet의 각 행을 객체로 매핑하는 역할을 한다. 이 코드는 콜백 패턴을 사용하여, SQL 쿼리의 결과로 얻어진 ResultSet에서 데이터를 읽어 Member 객체를 생성하고 반환하는 과정을 정의한다.

 

private RowMapper<Member> memberRowMapper() 메서드는 RowMapper<Member> 타입의 객체를 반환한다. 이 객체는 ResultSet에서 데이터를 읽어 Member 객체로 변환하는 방법을 정의한다.

  • (rs, rowNum) -> 람다 표현식을 사용하여 RowMapper의 mapRow 메서드를 구현한다. 여기서 rs는 쿼리 결과로 얻어진 ResultSet 객체이고, rowNum은 현재 행의 번호이다.
  • Member member = new Member(); 새로운 Member 객체를 생성한다.
  • member.setMemberId(rs.getString("member_id")); ResultSet에서 "member_id" 컬럼의 값을 읽어 Member 객체의 memberId 필드에 설정한다.
  • member.setMoney(rs.getInt("money")); ResultSet에서 "money" 컬럼의 값을 읽어 Member 객체의 money 필드에 설정한다.
  • return member; 매핑된 Member 객체를 반환한다.

 

JdbcTemplate 은 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다.

그 뿐만 아니라 트랜잭션을 위한 커넥션 동기화는 물론이고, 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다.

 

댓글