서비스 계층에 존재하는 예외에 대한 의존까지 없애려면 어떻게 해야 할까?
체크 예외인 SQLException
을 런타임 예외로 바꿔서 던진다면, 서비스에서 더이상 특정 기술에 대한 체크 예외에 종속적이지 않게 된다.
또한, 체크 예외는 메서드 뿐만 아니라 아래와 같이 인터페이스에도 throws
를 선언해줘야 한다는 단점이 있었는데 이 또한 해결이 가능해진다.
public interface MemberRepositoryEx {
Member save(Member member) throws SQLException;
Member findById(String memberId) throws SQLException;
void update(String memberId, int money) throws SQLException;
void delete(String memberId) throws SQLException;
}
public class MyDbException extends RuntimeException {
...
}
@Override
public Member save(Member member) {
try {
con = getConnection();
pState = con.prepareStatement(sql);
pState.setString(1, member.getMemberId());
pState.setInt(2, member.getMoney());
pState.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
throw new MyDbException(e); // 기존 루트 원인을 알 수 있게 포함해서 전달
} finally {
close(con, pState, null);
}
}
MyDbException
은 직접 만든 예외이기 때문에, JDBC나 JPA 같은 특정 기술에 종속적이지 않아서 서비스 계층의 순수성을 유지할 수 있다.
만약 특정 데이터베이스에 오류에 따라서 특정 예외는 복구하고 싶다면?
예를 들어 PK 중복이 일어났을때 오류가 아닌 자동으로 다른 ID를 배정해서 DB에 저장해서 정상 흐름으로 변경하고 싶다 가정해보자. 이때, SQLException
내부에 존재하는 오류 코드를 가져와서 어떤 오류인지 판별 후, 런타임 예외로 바꿔서 다시 던져주면 된다.
public class MyDuplicateKeyException extends MyDbException {
...
}
static class Repository {
try {
} catch (SQLException e) {
// h2 db SQL ErrorCode
if (e.getErrorCode() == 23505) { // 키 중복인 경우에 처리
throw new MyDuplicateKeyException(e);
}
throw new MyDbException(e);
}
}
하지만 SQL ErrorCode
의 정의는 데이터베이스마다 모두 다르기 때문에 DB가 변경된다면 레파지토리에 존재하는 모든 에러 코드들도 변경이 일어난다.
또한, 수백가지 에러코드에 대해 모든 상황에 맞는 런타임 예외를 생성해야 할까?
역시 스프링은 이런 문제들을 예외 추상화를 통해 해결해준다.
위에서처럼 직접 예외를 만들 필요 없이, 스프링에서는 이미 데이터베이스 접근 계층에 대한 예외들을 모두 정리해서 일관된 예외 계층을 제공하고 있다.
참고로 아래 그림은 일부 계층들은 생략된 상태이다.
NonTransient
예외 : 일시적이지 않은 예외이다. 같은 SQL을 그대로 반복해서 실행하면 실패한다. ex) SQL 문법 오류, 데이터베이스 제약조건 위배Transient
예외 : 일시적인 예외이다. 즉, 동일한 SQL을 다시 시도했을 때 성공할 가능성이 있다. ex) 쿼리 타임아웃, 락과 관련된 오류들스프링은 데이터베이스에서 발생한 오류 코드를 스프링이 정의한 예외로 바꾸어 주는 작업을 자동으로 처리해주는데, 이를 예외 변환기라고 한다. 위에서 직접 if
문으로 코드와 맞는지 확인하는 작업이 없어진다고 보면 된다.
@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); // 어떤 DB를 사용하는지 정보 얻어야 하기 때문에
}
@Override
public Member save(Member member) {
// SQL 문법 에러
String sql = "insertt into member(member_id, money) values(?, ?)";
Connection con = null;
PreparedStatement pState = null;
try {
con = getConnection();
pState = con.prepareStatement(sql);
pState.setString(1, member.getMemberId());
pState.setInt(2, member.getMoney());
pState.executeUpdate();
return member;
} catch (SQLException e) {
log.error("db error", e);
DataAccessException dex = exTranslator.translate("save", sql, e); // 스프링 예외 추상화
throw dex;
} finally {
close(con, pState, null);
}
}
...
}
exTranslator.translate()
: 문법이 잘못 된 상황이기 때문에, 발생한 예외에서 ErrorCode
를 보고 BadSqlGrammarException
으로 자동 변환해서 반환해준다.sql
, 3)Exception
을 전달하면 된다.각각의 데이터베이스마다 SQL ErrorCode
는 다른데, 어떻게 스프링은 각각의 DB
가 제공하는 SQL ErrorCode
까지 고려해서 예외를 변환할 수 있을까?
sql-error-codes.xml
해당 파일 안에는 거의 모든 관계형 데이터베이스의 에러 코드가 정의되어 있다.
스프링 SQL 예외 변환기가 SQL ErrorCode
를 바로 이 파일에 대입해서 어떤 스프링 데이터 접근 예외로 전환해야 할지 찾아내는 방식으로 동작하고 있는 것이다.
참고로 JdbcTemplate
을 사용한다면, 템플릿 콜백 패턴을 통해 위의 모든 과정들을 대신 처리해주기 때문에 매우 편리해진다.
public class MemberRepositoryV5 implements MemberRepository {
private final JdbcTemplate template;
public MemberRepositoryV5(DataSource dataSource) {
this.template = new JdbcTemplate(dataSource);
}
...
}