[Spring Database] 데이터베이스 예외 처리
서비스가 처리할 수 없는 SQLException 예외를 런타임 예외로 전환하는 방식으로 의존 관계를 제거하자.
MemberRepository 클래스도 인터페이스로 변경해 구현 기술을 쉽게 변경할 수 있도록 하자.
이제 MemberService의 코드 변경 없이 DI를 사용해 구현 기술을 변경할 수 있다.
인터페이스의 구현체가 체크 예외를 던지려면 인터페이스의 메서드에 먼저 체크 예외를 던지는 부분이 선언돼있어야 한다.
즉, SQLException과 같이 체크 예외를 사용하는 경우 인터페이스에도 해당 체크 예외가 선언돼있어야 한다. (throws Exception)
그런데, 인터페이스에 throw ~~Exception 문장을 추가하는 순간 해당 인터페이스는 특정 기술에 의존하게 되고, 이건 우리가 의도한 부분이 아니다.
의존 관계에서 자유로운 런타임 예외를 사용해 인터페이스에서도 종속을 끊어내자.
@Slf4j
public class MemberRepositoryV4_1 implements MemberRepository {
private final DataSource dataSource;
public MemberRepositoryV4_1(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) { // throw 삭제됨
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 new MyDbException(e); // 원인 넣어주기 e
} finally {
close(con, pstmt, null);
}
}
@Override
public Member findById(String memberId) {
String sql = "select * from member where member_id = ?";
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
rs = pstmt.executeQuery();
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) {
throw new MyDbException(e);
} finally {
close(con, pstmt, rs);
}
}
@Override
public void update(String memberId, int money) {
String sql = "update member set money=? where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, money);
pstmt.setString(2, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
@Override
public void delete(String memberId) {
String sql = "delete from member where member_id=?";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, memberId);
pstmt.executeUpdate();
} catch (SQLException e) {
throw new MyDbException(e);
} finally {
close(con, pstmt, null);
}
}
private void close(Connection con, Statement stmt, ResultSet rs) {
JdbcUtils.closeResultSet(rs);
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, dataSource);
}
private Connection getConnection() {
Connection con = DataSourceUtils.getConnection(dataSource);
log.info("get connection={} class={}", con, con.getClass());
return con;
}
}
MemberRepository 인터페이스를 구현한다.
기존에 SQLException을 던지는 부분을 런타임 예외인 MyDbException으로 변환해서 던져준다.
(기존 예외를 포함해야 되는걸 잊지 말자)
이제 서비스 부분에서도 throw SQLException을 제거할 수 있다.
대부분의 예외는 복구가 불가능하지만, 데이터베이스 오류에 따라서 특정 예외는 복구해야 하는 경우가 있다.
예를 들어 데이터베이스 서버가 열리지 않은 경우는 복구가 불가능하지만, 클라이언트가 회원 가입 시 중복되는 아이디를 입력한 경우 발생한 예외는 복구할 수 있다.
데이터베이스 서버에서 오류 코드를 반환하고, 오류 코드를 받은 JDBC 드라이버는 SQLException을 던진다. (오류 코드는 데이터베이스마다 다르다)
이 SQLException에는 데이터베이스가 제공하는 오류 코드가 담겨있는데, 서비스 계층에서 오류 코드를 확인하고 복구할 수 있는 예외이면 복구하는 방식으로 동작하면 되는데...
SQLException을 서비스 계층으로 던지는 순간 서비스 계층이 SQLException기술에 의존하게 되면서 또 의존관계가 주입된다.
그러니 리포지토리에서 오류 코드를 확인하고 복구 가능한 예외이면 예외를 변환해서 던지는 방식을 사용한다.
(SQLException -> MyDuplicateKeyException)
static class Service {
private final Repository repository;
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);
}
}
@RequiredArgsConstructor
static class Repository {
private final DataSource dataSource;
public Member save(Member member) {
String sql = "insert into member(member_id, money) values(?, ?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
con = dataSource.getConnection();
pstmt = con.prepareStatement(sql);
pstmt.setString(1, member.getMemberId());
pstmt.setInt(2, member.getMoney());
pstmt.executeUpdate();
return member;
} catch (SQLException e) {
//h2 db
if (e.getErrorCode() == 23505) {
throw new MyDuplicateKeyException(e);
}
throw new MyDbException(e);
} finally {
closeStatement(pstmt);
closeConnection(con);
}
}
}
직접 정의한 MyDuplicateKeyException 예외가 발생하면 아이디가 중복된 것으로, 아이디 뒤에 0~10000 사이 숫자를 붙여 다시 회원가입을 시도한다.
스프링은 데이터 접근 계층에 대한 수십 가지 예외를 정리해서 일관된 예외 계층을 제공하고, 각각의 예외는 특정 기술에 종속적이지 않도록 설계돼있다.
따라서 개발자가 직접 예외를 만들 필요 없이 스프링이 제공하는 예외를 가져다 사용하면 된다.
런타임 예외를 최고 조상으로 설정한다.
스프링이 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다.
Transient : 해당 예외와 자손은 동일한 SQL을 실행했을 때 성공할 가능성이 있는 예외이다. (쿼리 타임아웃, 락)
NotTransient : 일시적이지 않다는 뜻이다. 동일한 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) {
assertThat(e.getErrorCode()).isEqualTo(42122);
//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);
}
}
SQLExceptionTranslator를 사용해 예외를 자동으로 변환할 수 있다.
해당 객체의 translate 메서드에 작업명, sql, 예외를 넣어주면 스프링이 제공하는 예외로 적절하게 변환된다.
보이는 반환 타입은 최상위 타입인 DataAccessException이고, 실제로 반환되는 타입은 상황에 따라 다르다.
변환 작업 시 스프링은 데이터베이스별 에러 코드를 저장해 둔 sql-error-codes.xml 파일을 사용해 발생한 SQL 에러 코드가 어떤 예외에 해당하는지 찾는다.
서비스 혹은 컨트롤러 계층에서 예외 처리가 필요하면 특정 기술에 종속적인 예외를 사용하는 대신 스프링이 제공하는 데이터 접근 예외를 사용해 의존관계를 제거하자.
이제 DI를 제대로 사용할 수 있게 됐다.
'Spring > Spring Database' 카테고리의 다른 글
[Spring Database] 데이터베이스 테스트 (2) | 2022.09.07 |
---|---|
[Spring Database] JdbcTemplate (0) | 2022.09.06 |
[Spring Database] Transaction AOP (1) | 2022.09.01 |
[Spring Database] Transaction / Lock (1) | 2022.09.01 |
[Spring Database] Connection Pool과 Data Source (0) | 2022.08.31 |
댓글
이 글 공유하기
다른 글
-
[Spring Database] 데이터베이스 테스트
[Spring Database] 데이터베이스 테스트
2022.09.07 -
[Spring Database] JdbcTemplate
[Spring Database] JdbcTemplate
2022.09.06 -
[Spring Database] Transaction AOP
[Spring Database] Transaction AOP
2022.09.01 -
[Spring Database] Transaction / Lock
[Spring Database] Transaction / Lock
2022.09.01