지난 포스팅까지 자바 예외와 직접 예외를 생성하여 순수 서비스 로직을 유지하는 방법에 대해서 알아보았다.
이번 포스팅에서는 스프링에서 제공하는 예외 추상화에 대해 알아보고 적용해 보려고 한다.
지난 포스팅에서 남아있던 문제는 발생 에러에 따라 예외를 따로 처리하기는 힘들다는 문제가 있었다.
예를 들어 데이터베이스는 여러 제약사항에 따라 각각 다르 에러 코드를 가지고 있고, 또 같은 에러라도 데이터베이스마다 반환하는 코드는 각각 다르다. 그렇다면 개발자는 예외를 잡아 무언가 복구를 시도하려고 하면 데이터베이스별로, 그리고 각 에러별로 해결하는 로직을 수십 수백개를 짜야한다.
이건 아마 전세계 모든 개발자들이 겪는 문제이고, 여러 편리한 기능을 제공하는 스프링답게 스프링에서 이런 예외에 대한 추상화를 제공하고 있어 개발자는 이 기능을 가져다가 사용하기만 하면 된다!!
그럼 스프링 추상화와 적용 방법에 대해 알아보자
@Slf4j
public class SpringExceptionTranslatorTest {
DataSource dataSource;
@BeforeEach
void init() {
dataSource = new DriverManagerDataSource(URL, USERNAME, PASSWORD);
}
// 변환기 사용 X
@Test
void sqlExceptionErrorCode() {
String sql = "select bad grammer";
try {
Connection con = dataSource.getConnection();
PreparedStatement stmt = con.prepareStatement(sql);
stmt.executeUpdate();
} catch (SQLException e) {
assertThat(e.getErrorCode()).isEqualTo(42122);
int errorCode = e.getErrorCode();
log.info("errorCode={}", errorCode);
log.info("error", e);
}
}
// 변환기 사용
@Test
void exceptionTranslator() {
String sql = "select bad grammar";
try {
Connection con = dataSource.getConnection();
PreparedStatement stmt = con.prepareStatement(sql);
stmt.executeUpdate();
} catch (SQLException e) {
assertThat(e.getErrorCode()).isEqualTo(42122);
SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("작업명", sql, e);
log.info("resultEx", resultEx);
assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
}
translate("기능 설명", sql, exception);
위의 순서대로 파라미터를 넘겨주면
DataAccessException라는 최상위 예외를 리턴값으로 생성하는데,
실제로 테스트로 확인해보면 DataAccessException안에는 BadSqlGrammarException라는 sql문법 오류에 대한 구체적인 예외가 들어있는 것을 확인할 수 있다.
스프링에서는 sql 에러 코드를 파일로 관리하고 있는데,
이처럼 수 많은 데이터베이스에 대한 에러 코드를 정의하고 있어 지원하는 데이터베이스에서는 종류에 상관없이 발생하는 에러 코드에 대한 예외를 반환해준다.
그럼 직접 Repository에 예외변환기를 적용해보자.
@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) {
// BadSqlGrammarException 발생
// String sql = "insertxxxx into member(member_id, money) values (?,?)";
String sql = "insert into member(member_id, money) values (?,?)";
Connection con = null;
PreparedStatement pstmt = null;
try {
// 생략
} catch (SQLException e) {
throw exTranslator.translate("save", sql, e);
}finally {
close(con, pstmt, null);
}
}
이걸로 서비스가 특정 기술에 의존하는 문제와 데이터베이스 종류에 따른 예외 대응의 문제는 해결이 되었다.
그런데 코드를 보면 데이터베이스에 접근하는 메서드에서 반복이 되는 로직이 눈에 보인다. 커넥션을 얻는 부분이나, PreparedStatement, close()구문 등등은 데이터베이스에 접근하는 로직이라면 중복되는 로직들이다.
이러한 부분은 템플릿을 사용하면 해결이 되는데, 여기서는 JDBC를 사용하고 있으므로 JdbcTemplate을 활용해보자.
@Slf4j
public class MemberRepositoryV5 implements MemberRepository {
private final JdbcTemplate template;
public MemberRepositoryV5(DataSource dataSource) {
this.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;
}
// 생략
이번 포스팅에서는 스프링에서 제공하는 예외 추상화와 예외 변환기로 데이터 접근 기술이 변경되어도 서비스 계층의 순수성을 유지할 수 있고, 별도의 예외 클래스를 생성하지 않아도 여러 데이터베이스에서 발생하는 에러 코드에 상관없이 예외를 처리할 수 있게 되었다.
또한 JdbcTemplate을 활용해 중복되는 코드를 제거했을 뿐 아니라, 별도로 예외처리를 해주지 않아도 템플릿에서 예외처리까지 해주는 것을 확인하였다.
출처 : 스프링 DB 1편-김영한