1. 알고자 하는 것
- 일반적으로 jdbc를 사용해 쿼리를 수행하면 다음과 같이 Checked Exception인 SQLException이 필연적으로 throws 된다.
public void deleteAll() throws SQLException {
Connection c = null;
PreparedStatement ps = null;
// getConnection, prepareStatement, executeUpdate 모두 SQLException을 throws 한다.
c = dataSource.getConnection();
ps = c.prepareStatement("delete from users");
ps.executeUpdate();
// 자원 정리(close) 코드는 편의상 생략.
}
- 그러나 스프링에서 제공하는 JdbcTemplate을 사용해 쿼리를 수행하면 SQLException을 throws 하지 않는다.
public void deleteAllJdbcTemplate() { // throws SQLException이 없다.
jdbcTemplate.update("delete from users");
}
- 스프링의 JdbcTemplate은 SQLException을 어떻게 처리하는지 알아본다.
- 또한, 왜 그러한 방식으로 처리하는지도 함께 알아본다.
2. 알게된 것
스프링의 JdbcTemplate은 SQLException을 어떻게 처리하는지
- 스프링의 JdbcTemplate 내부 코드를 들어가보면 모든 메서드가 다음과 같이 런타임 예외인 DataAccessException을 throws 한다.
public int update(final String sql) throws DataAccessException {
....
}
// DataAccessException은 RuntimeException을 상속받는다.
public abstract class DataAccessException extends NestedRuntimeException {
public DataAccessException(String msg) {
super(msg);
}
public DataAccessException(@Nullable String msg, @Nullable Throwable cause) {
super(msg, cause);
}
}
- update 메서드 내부에서 사용하는 execute 메서드를 보면 SQLException을 스프링 JdbcTemplate이 어떻게 처리하는지 보여준다.
@Nullable
private <T> T execute(StatementCallback<T> action, boolean closeResources) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
Statement stmt = null;
Object var12;
try {
stmt = con.createStatement();
this.applyStatementSettings(stmt);
T result = action.doInStatement(stmt);
this.handleWarnings(stmt);
var12 = result;
} catch (SQLException var10) {
// try-catch 문을 통해 SQLException을 DataAccessException으로 전환한다.
String sql = getSql(action);
JdbcUtils.closeStatement(stmt);
stmt = null;
DataSourceUtils.releaseConnection(con, this.getDataSource());
con = null;
throw this.translateException("StatementCallback", sql, var10);
} finally {
if (closeResources) {
JdbcUtils.closeStatement(stmt);
DataSourceUtils.releaseConnection(con, this.getDataSource());
}
}
return var12;
}
- translateException이라는 내부 메서드를 통해 SQLException을 DataAccessException으로 전환한다.
- translateException 메서드는 DB별 에러코드를 분류해 스프링이 정의한 예외 클래스와 매핑해놓은 에러코드 매핑정보 테이블을 활용해 발생한 에러 코드에 맞는 DataAccessException 계층구조 내 서브 클래스 중 하나로 매핑해 해당 예외를 throws한다.
왜 이러한 방식으로 SQLException을 처리하는지
1. 복구가 불가능한 Checked Exception을 가능한 빠르게 Runtime Exception으로 전환하기 위해
- SQLException은 대부분 프로그램 오류, 개발자 실수 등 코드레벨에서 복구가 불가능한 예외이다.
- 복구가 불가능한 예외를 밖으로 던져도 이를 처리할 수 있는 가능성이 없다.
- 따라서 기계적인 throws 선언을 해야하는 Checked Exception을 가능한 빨리 Runtime Exception으로 전환해야 한다.
- 스프링의 JdbcTemplate은 이러한 모든 SQLException을 런타임 예외인 DataAccessException으로 전환한다.
2. 로우레벨의 Exception을 의미있는 Exception으로 전환하기 위해
- jdbc는 자바를 통해 DB에 접근하는 방법을 추상화된 API 형태로 정의한다.
- 각 DB 벤더가 jdbc 표준에 해당하는 드라이버를 만들어 제공함으로써 DB 종류에 관계없이 일관된 방법으로 DB에 접근이 가능하다.
- 그러나 DB 종류에 관계없이 사용할 수 있는 DB 접근 코드를 작성하는 것은 완벽하고 유연할 수 없다.
- DB마다 독립적인 예외의 종류와 원인이 존재한다.
- 그래서 jdbc는 DB마다 독립적이고 제각각인 예외를 SQLException이라는 예외로 모두 담아 처리한다.
- 스프링의 JdbcTemplate은 이러한 로우 레벨의 SQLException에 대해 DB별 에러코드를 분류한다.
- 스프링이 정의한 예외 클래스와 매핑해놓은 에러코드 매핑정보 테이블을 활용해 발생한 에러 코드에 맞는 DataAccessException 계층구조 내 서브 클래스 중 하나로 매핑한다.
- ex) SQL 문법 오류 시 -> BadSqlGrammerException
- ex) DB 커넥션 가져오기 실패 -> DataAccessResourceFailureException
- 이를 통해 예외에 대해 명시적으로 예외의 종류를 구분할 수 있고, catch 문을 분기하여 독립적인 처리가 가능하다.
- 또한, 메서드를 사용하는 클래스에서 기술에 의존하는 Exception을 명시적으로 throws 하지 않을 수 있다.
- 즉, 데이터 액세스 기술과 구현방법에 독립적인 이상적인 DAO를 만들 수 있다.
3. 정리
- 스프링의 JdbcTemplate은 기존 jdbc의 Checked Exception인 SQLException을 Runtime Exception인 DataAccessException으로 예외를 전환하여 처리한다.
- 이는 복구가 불가능한 Checked Exception을 가능한 빠르게 Runtime Exception으로 전환하기 위해서이다.
- 또한, 다양한 DB의 예외를 하나의 SQLException으로 추상화 한 것을 DB 별 에러코드 매핑정보를 통해 명시적이고 일관된 DataAccessException 서브 클래스 예외로 매핑함으로써 명시적으로 예외를 구분하고 DB 기술에 독립적인 설계를 가능하게 한다.
Reference
'Spring' 카테고리의 다른 글
외부 메서드, 내부 메서드에 대한 @Transactional 트랜잭션 적용 결과 테스트 (0) | 2023.06.22 |
---|---|
Jsoup 라이브러리를 통한 정적 페이지 크롤링 (0) | 2023.03.18 |
Proxy로 생성되는 Service와 의존관계를 갖는 @Repository, JpaRepository는 Proxy일까? (2) | 2022.09.09 |
JWT를 사용한 로그인 및 Refresh Token을 활용한 로그인 상태 유지 (9) | 2022.07.12 |
username만 매칭되면 user 세션 생성 되는 문제 해결 - AuthenticationProvider를 통한 password 기반 인증 (0) | 2022.05.30 |