이 글은 강의 : 김영한님의 - "[스프링 DB 1편 - 데이터 접근 핵심 원리]"을 듣고 정리한 내용입니다. 😁😁
이전까지 DB에서 발생하는 예외들 중 원하는 예외만 서비스 계층에서 잡아서 처리할 수 있도록 사용자 정의 예외를 만들었다. 그렇지만 이 때 한 가지 문제점이 발생한다. 처리하고 싶은 DB 예외를 만들기 위해서는 각 DB에서 발생하는 예외들을 예외 변환 처리를 해줘야한다. 데이터 접근 계층이 수백개가 되면 이것은 아주 큰 문제로 다가온다. 따라서 이 부분의 해결이 필요해진다.
스프링은 이미 데이터 접근 예외 계층을 추상화해서 제공해준다. 따라서 개발자는 해당 예외 계층을 사용하기만 하면 된다 !!
🧨 스프링은 데이터 접근 계층에 대한 수많은 예외를 정리해서 일관된 예외 계층을 제공해 준다.
🧨 스프링이 제공하는 예외는 순수 특정 기술에 종속적이지 않다. 따라서 서비스 계층(Service)에서 스프링이 제공하는 예외를 사용했을 때, DB 접근 기술에 대한 종속성 문제는 해결된다.
🧨 스프링은 JDBC / JPA를 사용할 때 발생하는 예외를 스프링 예외 계층으로 변환해 준다.
🧨 스프링 데이터 접근 예외 계층의 최상위 클래스는 DataAcessException이다. 이 예외는 Runtime 예외를 상속받았으며, 따라서 스프링이 제공하는 모든 데이터 접근 계층 예외는 런타임 예외다.
🧨 DataAccessException의 분류
NonTransisentDataAccessException
Transient : 일시적.
TransisentDataAccessException
DataAccessException 클래스를 타고 들어가서 해당 클래스를 구현한 하위 계층을 확인할 수 있다. (수많은 예외가 존재한다.)
앞서 스프링은 다양한 DB에서 발생하는 예외를 특정 DB 기술에 종속되지 않는 런타임 예외 계층을 제공해준다고 했다. 그렇지만 이 모든 예외 계층을 개발자가 하나씩 분석해서 만드는 것은 리소스 낭비다. 따라서 스프링은 DB에서 발생하는 예외를 ErrorCode를 바탕으로 스프링이 제공하는 DB 계층 예외로 변경해주는 예외 변환기를 제공해준다. 예외 변환기를 사용하면 예외 변환도 자동으로 되고, 만들어지는 예외 모두 특정 DB 기술에 종속적이지 않기 때문에 DI를 유지하면서 개발 생산성도 높일 수 있다.
// 예외 변환기 선언
public MemberRepositoryV4_2(DataSource dataSource) {
this.dataSource = dataSource;
this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
// 예외 변환기 : 예외 변환
throw exTranslator.translate("find", sql, e);
🎈 translate를 이용해 발생한 예외를 분석해서 스프링이 제공하는 DB 계층 예외로 변경해준다.
실행하면 여기서 task [select]라고 되어 있는데 내가 task에 select를 입력했기에 이렇게 발생 ! → 따라서 로그에 남길 값을 task에 작성해두면 된다.
DataSource에서 얻어온 값을 바탕으로 ErrorCode를 분석해서 필요한 형식의 예외를 만들어준다. 이 때, BadSqlGrammerException으로 예외가 변환된다.
org.springframework.jdbc.support
경로로 가보면 sql-error-codes.xml 파일이 저장되어있다.SpringExceptionTranslator를 이용한 테스트 코드
//SpringExceptionTranslatorTest.java
@Test
void sqlExceptionErrorCode() {
String sql = "select bad grammer";
try {
Connection con = dataSource.getConnection();
PreparedStatement pstmt = con.prepareStatement(sql);
pstmt.executeQuery();
} catch (SQLException e) {
assertThat(e.getErrorCode()).isEqualTo(42122);
int errorCode = e.getErrorCode();
log.info("errorCode = {}", errorCode);
SQLErrorCodeSQLExceptionTranslator translator
= new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = translator.translate("select", sql, e);
log.info("resultEx = {}", resultEx);
assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
🎈 SQLExceptionTranslator를 만들어준다.
🎈 SQLExceptionTranslator에 발생한 Exception과 SQL을 넘겨주면, 현재 어떤 이유로 Exception이 발생했는지를 분석해서 Spring이 제공하는 DB 계층으로 변경해서 제공해준다.
이제 해당 SpringExceptionTranslator를 실제 코드에 적용해보자
// MemberServiceV4_2 필드 코드 수정
private final DataSource dataSource;
private final SQLExceptionTranslator exTranslator;
public MemberRepositoryV4_2(DataSource dataSource) {
this.dataSource = dataSource;
this.exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
// MemberServiceV4_2 : 코드 수정
@Override
public Member findById(String memberId){
String sql = "select * from member where member_id = ?";
...
} catch (SQLException e) {
log.error("error", e);
throw exTranslator.translate("find", sql, e); //이 부분 !!!
}finally {
...
}
}
🎈 스프링은 데이터 접근 계층에 대해서 일관된 예외 추상화를 제공해준다. 이 때 제공하는 예외는 DB 기술 종속적이지 않으며, 모두 런타임 예외다.
🎈 스프링은 예외 변환기를 제공해주고, 예외 변환기는 DataSource / SQL / Exception을 입력받아 ErrorCode에 맞는 적절한 스프링 데이터 접근 예외 계층으로 변환해준다.
🎈 스프링 예외 변환기로 만들어지는 예외는 런타임 예외 + 특정 기술 종속적이지 않으므로, 필요 시 Controller / Service 계층에서 사용해도 문제가 없다.
🎈 서비스 계층에서 발생하는 예외를 잡아서 복구해야하는 경우, 예외가 스프링이 제공하는 데이터 접근 계층 예외로 변환되서 넘어오기 때문에 필요한 경우 예외를 잡아서 복구하면 된다.
🧨 남은 문제
지금까지 Connection과 DB를 사용하는 방법을 추상화 했고, Transaction을 추상화했다. 그리고 마지막으로 예외까지 추상화를 했다. 그럼에도 불구하고 코드 상에 해결 여지가 남아있다. 바로 JDBC를 사용하기 때문에 발생하는 반복 문제의 해결이 남아있다.
리포지토리의 각각의 메서드를 살펴보면 상당히 많은 부분이 반복된다.
아직까지도 개선의 여지가 있음.
@Override
public Member findById(String memberId){
String sql = "select * from member where member_id = ?";
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
Connection conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
log.info("Connection = {}, class = {}", conn,conn.getClass());
if (rs.next()) {
Member member = new Member();
member.setMemberId(rs.getString("member_id"));
member.setMoney(rs.getInt("money"));
return member;
}else{
throw new NoSuchElementException("member not found memberId = " + memberId);
}
} catch (SQLException e) {
log.error("error", e);
throw exTranslator.translate("find", sql, e);
}finally {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(pstmt);
}
}
코드를 보면 계속 반복되는 부분이 존재.
🎈 스프링은 JDBC의 반복 문제를 해결하기 위해 **JdbcTemplate
템플릿 제공**.
위에서 발생하는 반복들은 대부분 JDBC 기술을 사용하기 때문에 발생하는 것이다. 이런 반복적인 부분들을 해결할 수 있다면, 코드가 아주 아름답게 리팩토링 될 것이다. 그렇다면 어떤 방식으로 접근할 수 있을까?
먼저 위 구문들은 실제 비즈니스 로직 사이에 적용되어있다. 따라서 이런 것들은 따로 메서드로 빼서 처리를 할 수 없다. 이런 경우에는 템플릿 콜백 패턴을 이용해서 처리할 수 있다. 다행히 이런 문제를 해결하기 위한 템플릿 콜백 패턴이 구현되어있다. 바로 JdbcTemplate이다. 위에서 발생한 문제들은 JdbcTemplate을 이용하면 깔끔하게 처리할 수 있다.
@Slf4j
public class MemberRepositoryV5 implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
public MemberRepositoryV5(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
String sql = "insert into member(member_id, money) values(?,?)";
jdbcTemplate.update(sql,member.getMemberId(), member.getMoney());
return member;
}
@Override
public Member findById(String memberId){
String sql = "select * from member where member_id = ?";
return jdbcTemplate.queryForObject(sql, memberRowMapper(), memberId);
}
@Override
public void update(String memberId, int money){
String sql = "update member set money = ? where member_id = ?";
jdbcTemplate.update(sql, memberId, money);
}
@Override
public void delete(String memberId){
String sql = "delete from member where member_id=?";
jdbcTemplate.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;
};
}
}
코드를 보면 JdbcTemplate는 JDBC로 개발할 때 발생하는 반복을 대부분 해결해준다 ! 그 뿐만 아니라 지금까지 학습했던, 트랜잭션을 위한 커넥션 동기화는 물론, 그 예외 발생시 스프링 예외 변환기도 자동으로 실행해준다 !!!
🎃 참고
🎈 정리
JdbcTemplate은 트랜잭션을 위한 커넥션 동기화 / 예외 발생 시 스프링 예외 변환기도 자동으로 실행해준다.
서비스 계층의 순수성
리포지토리에서 JDBC를 사용하는 반복 코드가 JdbcTemplate에 의해 제거되었다.
한번은 이렇게 전체 과정을 직접 해보는 것이 나중에 간단하게 쓸 때 문제가 생길 때 깊이있게 이해할 수 있어 !!!