Go, Vantage point
가까운 곳을 걷지 않고 서는 먼 곳을 갈 수 없다.
Github | https://github.com/overnew/
Blog | https://everenew.tistory.com/
티스토리 뷰
* 김영한님의 스프링 DB 1편 강좌를 수강하며 정리한 글입니다. *
이전 글에서는 스프링의 트랜잭션 관리에 대해 정리하였다.
우리는 예외처리는 문법서에서 배웠지만 실무에서 어떤 식으로 활용하는지는 전혀 배우지 못했다.
이번 글에는 예외의 활용법에 대해 정리해보자.
자바 예외 계층
Throwable은 최상위 예외이다.
Error는 JVM에서 throw되는 복구 불가능한 오류이다.
만약 Throwable을 catch로 잡으면 Error도 함께 잡히기 때문에 Exception부터 잡아야 한다.
Exception은 체크 예외로 컴파일러가 확인한다.
이 중에서 RuntimeException의 하위 예외들은 런타임 예외(언체크 예외)로 컴파일러가 확인하지 않고, 실행 시점에 발생한다.
예외는 처리가 안된다면 호출된 곳으로 던지게 된다.
서버에서 예외가 처리되지 않고 계속 넘어간다면 위험하기 때문에, WAS는 오류 페이지를 전달한다.
커스텀 예외 만들기
Exception을 상속하면 체크 예외를 만들 수 있다.
class MyCheckedException extends Exception{
public MyCheckedException(String message) {
super(message);
}
}
체크 예외는 반드시 예외 처리를 하거나 던져야한다.
예외 처리 구문의 누락은 컴파일러가 잡아주기 때문에 안정성이 보장된다.
하지만 수많은 라이브러리의 모든 예외를 일일이 처리해주는 과정은 간단하지 않다.
거기에 자신의 클래스의 역할과 맞지 않는 예외 처리도 throw 선언을 하기 위해 코드 의존관계가 생긴다.
RuntimeException을 상속하면 런타임(언체크) 예외를 만들 수 있다.
static class MyUncheckedException extends RuntimeException{
public MyUncheckedException(String message) {
super(message);
}
}
언체크 예외는 예외 처리 구문이 없다면 자동으로 상위로 throw를 하기 때문에 throw 선언을 생략할 수 있다.
throw 선언을 생략할 수도 있지만, 예외 발생 가능성을 알리기 위해 선언할 수도 있다.
신경 쓰고 싶지 않은 예외처리를 자동으로 밖으로 던져주기 때문에 의존관계가 없어진다.
하지만, 예외처리가 누락되더라도 컴파일러가 잡아주지 않는다.
런타임 예외 활용
트렌드상 기본적으로는 런타임 예외를 사용한다.
단, 반드시 잡아야 하는 비즈니스 로직 상의 예외는 체크 예외를 사용한다.
예를 들면 계좌이체의 실패와 같은 심각한 문제는 반드시 처리해 주어야 할 것이다.
왜 체크 예외보다 런타임 예외를 사용하는 것일까?
스프링 MVC 구조와 비즈니스 계층과 데이터 계층은 각각의 역할에 충실하다.
하부의 네트워크와 Repository에서의 문제를 비즈니스나 MVC 계층이 해결하는 것은 단일 책임 원칙에도 어긋난다.
거기에 throw로 던지더라도 SQLException과 같은 JDBC의 예외를 throw에 선언하면, 리포지토리 기술 변경 시(JPAException으로) 의존 중인 예외 코드를 모두 변경해야 한다.
따라서 런타임 예외가 자동으로 예외를 호출자에게 던지는 것을 이용하여, 최상위에서 예외를 공통으로 처리해주는 ControllerAdvice로 해결한다. 이때 클라이언트에게는 HTTP 500 상태 코드 응답으로 대처한다.
이제 기술 변경으로 인한 예외 코드 변경은 공통 처리 부분에서만 일어나게 된다.
대부분의 라이브러리들이 런타임 예외로의 전환이 이루어졌다.
따라서 런타임 예외 부분들은 문서화해 둔 것이 많다.
스프링은 중요한 예외의 경우, throw로 선언을 해두었다.
런타임 예외로 전환
기존의 체크 예외도 런타임 예외로 전환시킬 수 있다.
static class Repository{
public void call(){
try {
runSQL();
}catch (SQLException e){
//런타임으로 바꿔서 던짐 -> 체크예외 내용을 내부에 포함
throw new RuntimeSQLException(e);
}
}
public void runSQL() throws SQLException {
throw new SQLException("ex");
}
}
//throwable로 기존 예외 포함가능
static class RuntimeSQLException extends RuntimeException{
public RuntimeSQLException(Throwable cause){
super(cause);
}
}
Throwable은 내부에 예외를 넣을 수 있다.
따라서 RuntimeSQLException 런타임 예외로 체크 예외를 감싸면 런타임 예외로 바꾸어 던질 수 있다.
Throwable은 이후 오류 메시지를 출력하면 내부의 예외 메시지까지 출력해준다.
만약 런타임 예외를 이용해 Throwable로 체크 예외를 감싸지 않으면 어떤 오류가 발생했는지 전혀 알 수 없다.
스프링 예외 변환기와 JdbcTemplate
이번에는 서비스 코드의 예외 의존성을, 런타임 예외로 전환하여 제거해보자.
체크 예외는 인터페이스로의 추상화에서도 발목을 잡는다.
인터페이스의 메서드 선언부에서도 throw 예외를 선언해 주어야하기 때문이다.
이러한 문제는 인터페이스가 특정 기술의 예외 의존성을 가지게 되어, 추상화의 의미를 퇴색시킨다.
하지만 런타임 예외는 선언을 강제하지 않기 때문에 추상화에 적합하다.
모든 오류가 공통 처리 부분에서 해결돼야 하는 것은 아니다.
아이디의 중복 오류는 서비스 계층에서 새로운 아디를 다시 만들어야 할 필요가 있다.
이런 경우 DB의 에러코드로 중복을 파악하는데, 문제는 DB 마다 모두 에러 코드의 번호와 의미가 다르다는 것이다.
이를 해결하기 위해 스프링은 데이터 접근 예외를 추상화하여 제공한다.
스프링은 예외를 분석하여 자동으로 스프링의 런타임 예외로 변환해주는 예외 변환기를 제공한다.
SQLErrorCodeSQLExceptionTranslator
String sql ="select bad grammer";
SQLErrorCodeSQLExceptionTranslator exTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
// 예외 분석 결과 잘못된 sql 구문 오류인 BadSqlGrammarException가 반환됨
DataAccessException resultEx = exTranslator.translate("select", sql, exception);
스프링 데이터 예외인 DataAccessException으로는 모든 예외를 받을 수 있고, 변환 결과가 반환된다.
이제 데이터 관령 오류를 translate를 통해 변환 후 넘기는 작업으로 대체할 수 있다.
} catch (SQLException e) {
throw exTranslator.translate("작업 내용", sql, e);
}...
JdbcTemplate를 사용하면 CRUD의 작업도 간편화해준다.
public Member save(Member member){
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(); //실제 db로 sql 전달
return member;
} catch (SQLException e) {
throw exTranslator.translate("save", sql, e);
}finally {
close(con,pstmt,null);
}
}
//JdbcTemplate 사용 시
private final JdbcTemplate template;
public Member save(Member member){
String sql = "insert into member(member_id, money) values(?, ?)";
template.update(sql, member.getMemberId(), member.getMemberId());
return member;
}
JdbcTemplate는 쿼리문의 실행, 커낵션 close등의 작업을 모두 실행해준다.
특히 예외를 자동으로 스프링 예외 변환기를 진행해 주기 때문에 굉장히 편리하다.
'개발 > Spring DataBase' 카테고리의 다른 글
[Spring DB] 7. DataBase Test, 임베디드 DB (0) | 2022.07.25 |
---|---|
[Spring DB] 6. 프로필과 JdbcTemplate (0) | 2022.07.24 |
[Spring DB] 4. 스프링의 트랜잭션 매니저 (0) | 2022.07.19 |
[Spring DB] 3. 트랜잭션과 ACID, DB Lock (0) | 2022.07.18 |
[Spring DB] 2. 커넥션 풀과 DataSource (0) | 2022.07.16 |