스프링 예외 추상화
예외가 발생했을 때 해당 예외를 그대로 반환하는 것이 아니라 런타임 예외를 따로 만들어서 해당 예외를 반환하는 것이 효율적이다.
@Test
void duplicateKeySave() {
service.create("myId");
service.create("myId"); // 같은 아이디 저장 시도
}
@Slf4j
@RequiredArgsConstructor
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 {
JdbcUtils.closeStatement(pstmt);
JdbcUtils.closeConnection(con);
}
}
}
같은 아이디를 반복 저장하는 상황이다. service에서 반복되는 아이디를 저장한다는 요청이 들어오면 random으로 인해 10000까지의 숫자를 원래 아이디 뒤에 붙여준다. 이 때 에러코드가 23505일 때 MyDuplicateKeyException(e)로 반환하여서 중복 여부를 판단할 수 있다. 이처럼 특별한 예외가 발생했을 때에는 해당 예외처리를 할 수 있는 런타임 예외를 만들어서 반환해주면 된다. 하지만 에러 코드도 db마다 다르고 모든 예외를 저렇게 처리할 수 없으니 스프링에서 지원하는 예외 추상화를 사용하는 것이 좋다.
스프링 예외 추상화
@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);
SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
// 스프링이 제공하는 SQL 예외 변환기
log.info("resultEx", resultEx);
assertThat(resultEx.getClass()).isEqualTo(BadSqlGrammarException.class);
}
}
SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
DataAccessException resultEx = exTranslator.translate("select", sql, e);
위의 두 줄은 스프링이 제공하는 SQL 예외 변환기이다. 문법 오류가 발생하면 ErrorCode가 42122로 발생하는데 이러한 예외코드들을 db별로 저장해두어서 해당 예외가 발생했을 때 알맞은 예외를 반환해준다. db별로 의존하지도 않기 때문에 추후에 db를 바꿔준다해도 별도의 코드 수정을 최소화로 할 수 있다. 위의 코드는 실제 코드가 아니라 스프링 예외 추상화만을 보기 위한 테스트코드이다.
@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) {
String sql = "insert into member(member_id, money) values (?, ?)";
// (?, ?)가 아니라 (memberId, money)라 하면 보안상의 위험
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 exTranslator.translate("save", sql, e);
} finally {
close(con, pstmt, null);
// 항상 close가 되도록 finally에서 호출해야 함
}
}
원래 코드인 save에 exTranslator.translate 메서드를 통해 스프링 예외 추상화를 진행했다.