JdbcTemplate
을 대표로 하는 스프링의 데이터 액세스 기능에 담겨 있는 예외처리와 관련된 접근 방법에 대해 알아보자.
SQLException
JdbcContext
에서 JdbcTemplate
으로 바꾸기 전과 후의 deleteAll()
메서드를 비교해보자.
JdbcTemplate
적용 이전에는 있었던 throws SQLException
선언이 적용 후에는 사라졌다.먼저 개발자들의 코드에서 종종 발견되는 초난감 예외처리의 사례를 보자.
예외가 발생하면 그것을 catch
블록을 써서 잡아낸 후에 아무것도 하지 않고 별문제 없는 것처럼 넘어가는 것은 정말 위험한 일이다. 왜냐하면 프로그램 실행 중에 어디선가 오류가 있어서 예외가 발생했는데 그것을 무시하고 계속 진행해버리기 때문이다. 결국 발생한 예외로 인해 어떤 기능이 비정상적으로 동작하거나, 메모리나 리소스가 소진되거나, 예상치 못한 다른 문제를 일으킬 것이다. 더 큰 문제는 그 시스템 오류나 이상한 결과의 원인을 찾아내기가 매우 힘들다는 것이다.
📌 예외를 처리할 때 반드시 지켜야 할 핵심 원칙은 "모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다." 이다.
throws
throws Exception
이 선언되어 있는 메서드에서는 의미 있는 정보를 얻을 수 없다.throws Exception
을 따라서 붙이는 수밖에 없다. 자바에서 throw
를 통해 발생시킬 수 있는 예외는 크게 세 가지가 있다.
Error
java.lang.Error
클래스의 서브클래스들이다.catch
블록으로 잡아봤자 아무런 대응 방법이 없기 때문이다. Exception
과 체크 예외java.lang.Exception
클래스와 그 서브클래스들이다. Exception
클래스는 체크 예외와 언체크 예외로 구분된다. Exception
클래스의 서브클래스이면서 RuntimeException
클래스를 상속하지 않은 것들RuntimeException
을 상속한 클래스RuntimeException
은 Exception
의 서브클래스지만 자바는 이 RuntimeException
과 그 서브클래스는 특별하게 다룬다. RuntimeException
과 언체크/런타임 예외java.lang.RuntimeException
클래스를 상속한 예외catch
문으로 잡거나 throws
로 선언해도 되고 안해도 된다.NullPointerException
: 오브젝트를 할당하지 않은 레퍼런스 변수를 사용하려고 시도했을 때 발생한다.IllegalArgumentException
: 허용되지 않는 값을 사용해서 메서드를 호출할 때 발생한다.catch
나 throws
를 사용하지 않아도 되도록 만든 것이다. 예외를 처리하는 일반적인 방법을 살펴보자.
사용자가 요청한 파일을 읽으려고 시도했는데 해당 파일이 없다거나 다른 문제가 있어 읽히지가 않아서 IOException
이 발생했다고 생각해보자.
IOException
에러 메시지가 사용자에게 그냥 던져지는 것이다. 통제 불가능한 외부 요인으로 인해 예외가 발생하면 MAX_RETRY
만큼 재시도를 하는 간단한 예다. 사전에 미리 성공 여부를 확인할 수 없고, 재시도가 의미 있는 경우라면 이렇게 최대 횟수만큼 반복적으로 시도함으로써 예외상황에서 복구되게 할 수 있다.
throws
문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch
문으로 일단 예외를 잡은 후에 로그를 남기고 다시 예외를 던지는 것이다.throws SQLException
이 붙어 있다. 콜백 오브젝트의 메서드는 SQLException
에 대한 예외를 회피하고 템플릿 레벨에서 처리하도록 던져준다.1. 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우에, 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서다.
예) 새로운 사용자를 등록하려고 시도했을 때 아이디가 같은 사용자가 있어서 DB 에러가 발생하면 JDBC API는 SQLException
을 발생시킨다. 로그인 아이디 중복 같은 경우는 충분히 예상 가능하고 복구 가능한 예외상황이다. 이럴 땐 DAO에서 SQLException
의 정보를 해석해서 DuplicateUserIdException
같은 예외로 바꿔서 던져주는 게 좋다.
보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외로 만드는 것이 좋다. 중첩 예외는 getCause()
메서드를 이용해 처음 발생한 예외가 무엇인지 확인할 수 있다. 중첩 예외는 새로운 예외를 만들면서 생성자나 initCause()
메서드로 근본 원인이 되는 예외를 넣어주면 된다.
2. 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것이다.
중첩 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부에 담아서 던지는 방식은 같다. 하지만 의미를 명확하게 하려고 다른 예외로 전환하는 것이 아니다. 주로 예외처리를 강제하는 체크 예외를 런타임 예외로 바꾸는 경우에 사용한다.
예) EJBException
을 들 수 있다. EJB 컴포넌트 코드에서 발생하는 대부분의 체크 예외는 비즈니스 로직으로 볼 때 의미 있는 예외이거나 복구 가능한 예외가 아니다. 이런 경우에는 런타임 예외인 EJBException
으로 포장해서 던지는 편이 낫다.
📌
EJBException
은RuntimeException
클래스를 상속한 런타임 예외다. 이렇게 런타임 예외로 만들어서 전달하면 EJB는 이를 시스템 익셉션으로 인식하고 트랜잭션을 자동으로 롤백해준다. 런타임 예외이기 때문에 EJB 컴포넌트를 사용하는 다른 EJB나 클라이언트에서 일일이 예외를 잡거나 다시 던지는 수고를 할 필요가 없다. 이런 예외는 잡아도 복구할 만한 방법이 없기 때문이다.
반대로 애플리케이션 로직상에서 예외조건이 발견되거나 예외상황이 발생할 수도 있다. 이런 것은 API가 던지는 예외가 아니라 애플리케이션 코드에서 의도적으로 던지는 예외다. 이때는 체크 예외를 사용하는 것이 적절하다. 비즈니스적인 의미가 있는 예외는 이에 대한 적절한 대응이나 복구 작업이 필요하기 때문이다.
일반적으로 체크 예외를 계속 throws
를 사용해 넘기는 건 무의미하다. 복구가 불가능한 예외라면 가능한 한 빨리 런타임 예외로 포장해 던지게 해서 다른 계층의 메서드를 작성할 때 불필요한 throws
선언이 들어가지 않도록 해줘야 한다.
대부분 서버 환경에서는 애플리케이션 코드에서 처리하지 않고 전달된 예외들을 일괄적으로 다룰 수 있는 기능을 제공한다. 어차피 복구하지 못할 예외라면 애플리케이션 코드에서는 런타임 예외로 포장해서 던져버리고, 예외처리 서비스 등을 이용해 자세한 로그를 남기고, 관리자에게는 메일 등으로 통보해주고, 사용자에게는 친절한 안내 메시지를 보여주는 식으로 처리하는 게 바람직하다.
일반적으로는 체크 예외가 일반적인 예외를 다루고, 언체크 예외는 시스템 장애나 프로그램상의 오류에 사용한다고 했다.
자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다. 자칫하면 throws Exception
으로 점철된 아무런 의미도 없는 메서드들을 낳을 뿐이다. 그래서 대응이 불가능한 체크 예외라면 빨리 런타임 예외로 전환해서 던지는 게 낫다.
자바 초기부터 있었던 JDK의 API와 달리 최근에 등장하는 표준 스펙 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화되고 있다. 예전에는 복구할 가능성이 조금이라도 있다면 체크 예외로 만든다고 생각했는데, 지금은 항상 복구할 수 있는 예외가 아니라면 일단 언체크 예외로 만드는 경향이 있다. 언체크 예외라도 필요하다면 얼마든지 catch
블록으로 잡아서 복구하거나 처리할 수 있다. 하지만 대개는 복구 불가능한 상황이고 보나마자 RuntimeException
등으로 포장해서 던져야 할 테니 아예 API 차원에서 런타임 예외를 던지도록 만드는 것이다.
add()
메서드의 예외처리add()
메서드는 DuplicatedUserIdException
과 SQLException
, 두 가지의 체크 예외를 던지게 되어 있다.
DuplicatedUserIdException
과 SQLException
의 차이DuplicatedUserIdException
은 충분히 복구 가능한 예외이므로 add()
메서드를 사용하는 쪽에서 잡아서 대응할 수 있지만, SQLException
은 대부분 복구 불가능한 예외이므로 결국 throws
를 타고 계속 앞으로 전달되다가 애플리케이션 밖으로 던져질 것이다. 그럴 바에는 그냥 런타임 예외로 포장해 던져버려서 그 밖의 메서드들이 신경 쓰지 않게 해주는 편이 낫다.
DuplicatedUserIdException
같은 의미 있는 예외는 add()
메서드를 바로 호출한 오브젝트 대신 더 앞단의 오브젝트에서 다룰 수도 있다. 어디에서든 이 예외를 잡아서 처리할 수 있다면 굳이 체크 예외가 아닌 런타임 예외로 만드는 게 낫다. 대신 add()
메서드는 명시적으로 DuplicatedUserIdException
을 던진다고 선언해야 한다. 그래야 add()
메서드를 사용하는 코드를 만드는 개발자에게 의미 있는 정보를 전달해줄 수 있다.
이 방법을 이용해 add()
를 수정해보자.
DuplicatiedUserIdException
을 만든다. RuntimeException
을 상속한 런타임 예외로 만든다. add()
메서드에서 SQLException
을 런타임 예외로 전환해서 던지도록 만든다. SQLException
은 런타임 예외가 되어 메서드 선언의 throws
에 포함시킬 필요가 없다.DuplicatedUserIdException
외에 시스템 예외에 해당하는 SQLException
은 언체크 예외가 됐기 때문에 불필요한 throws 선언을 할 필요가 없어진다.add()
메서드를 사용하는 쪽에서 아이디 중복 예외를 처리하고 싶은 경우 활용할 수 있음을 알려주도록 DuplicatedUserIdException
을 메서드의 throws
선언에 포함시킨다. 📌 런타임 예외의 일반화
런타임 예외를 일반화해서 사용하는 방법은 여러모로 장점이 많다. 단, 런타임 예외로 만들었기 때문에 사용에 더 주의를 기울일 필요도 있다. 컴파일러가 예외처리를 강제하지 않으므로 신경 쓰지 않으면 예외상황을 충분히 고려하지 않을 수도 있기 때문이다. 런타임 예외를 사용하는 경우에는 API 문서나 레퍼런스 문서 등을 통해, 메서드를 사용할 때 발생할 수 있는 예외의 종류와 원인, 활용 방법을 자세히 설명해두자.
시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch
해서 무엇인가 조치를 취하도록 요구하는 예외가 있다. 이런 예외들을 일반적으로 애플리케이션 예외라고 한다.
예를 들어, 사용자가 요청한 금액을 은행계좌에서 출금하는 기능을 가진 메서드가 있다고 생각해보자. 여기선 현재 잔고를 확인하고, 허용하는 범위를 넘어서 출금을 요청하면 출금 작업을 중단시키고, 적절한 경고를 사용자에게 보내야 한다.
이런 기능을 담은 메서드를 설계하는 방법이 두 가지 있다.
try
블록 안에 정리해두고 예외상황에 대한 처리는 catch
블록에 모아둘 수 있기 때문에 코드를 이해하기도 편하다.InsufficientBalanceException
등을 던진다. InsufficientBalanceException
을 만들 때는 예외상황에 대한 상세한 정보를 담고 있도록 설계할 필요가 있다. 잔고가 부족한 경우라면 현재 인출 가능한 최대 금액은 얼마인지 확인해서 예외 정보에 넣어준다면 좋을 것이다. SQLException
은 어떻게 됐나?DAO에 존재하는 SQLException
에 대해 생각해보자.
99%의 SQLException은 코드 레벨에서는 복구할 방법이 없다. 프로그램의 오류 또는 개발자의 부주의 때문이거나 통제할 수 없는 외부상황 때문에 발생하는 것이다. 예를 들어 SQL 문법이 틀렸거나, 제약조건을 위반했거나, DB 서버가 다운됐거나, 네트워크가 불안정하거나, DB 커넥션 풀이 꽉 차서 DB 커넥션을 가져올 수 없는 경우 등이다.
시스템의 예외라면 당연히 애플리케이션 레벨에서 복구할 방법이 없다. 관리자나 개발자에게 빨리 예외가 발생했다는 사실이 알려지도록 전달하는 방법밖에는 없다. 마찬가지로 애플리케이션 코드의 버그나 미처 다루지 않았던 범위를 벗어난 값 때문에 발생한 예외도 역시 복구할 방법이 없다. 개발자가 빨리 인식할 수 있도록 발생한 예외를 빨리 전달하는 것 외에는 할 수 있는 게 없다.
이렇듯 대부분의 SQLException
은 복구가 불가능하기 때문에 예외처리 전략을 적용해야 한다. 필요도 없는 기계적인 throws
선언이 등장하도록 방치하지 말고 가능한 한 빨리 런타임 예외로 전환해줘야 한다.
스프링의 JdbcTemplate
은 바로 이 예외처리 전략을 따르고 있다. JdbcTemplate
템플릿과 콜백 안에서 발생하는 모든 SQLException
을 런타임 예외인 DataAccessException
으로 포장해서 던져준다. 따라서 JdbcTemplate
을 사용하는 UserDao
메서드에선 꼭 필요한 경우에만 런타임 예외인 DataAccessException
을 잡아서 처리하면 되고 그 외의 경우에는 무시해도 된다. 그래서 DAO 메서드에서 SQLException
이 모두 사라진 것이다.
JdbcTemplate
의 update()
, queryForInt()
, query()
메서드는 모두 throws DataAccessException
이라고 되어 있다. 하지만 런타임 예외이므로 update()
를 사용하는 메서드에서 이를 잡거나 다시 던질 의무는 없다.
public int update(final String sql) throws DataAccessException { ... }
그 밖에도 스프링의 API 메서드에 정의되어 있는 대부분의 예외는 런타임 예외다.
catch
/throws
를 줄여주는 것JdbcTemplate
이 던지는 DataAccessException
SQLException
을 런타임 예외로 포장해서 복구 불가능한 예외에 대해 애플리케이션 레벨에서 신경 쓰지 않도록 해준다.SQLException
에 담긴 다루기 힘든 상세한 예외정보를 의미 있고 일관성 있는 예외로 추상화해준다.JDBC는 자바를 이용해 DB에 접근하는 방법을 추상화된 API 형태로 정의해놓고, 각 DB 업체가 JDBC 표준을 따라 만들어진 드라이버를 제공하게 해준다. 하지만 DB 종류에 상관없이 사용할 수 있는 데이터 액세스 코드를 작성하는 일은 쉽지 않다. 현실적으로 DB를 자유롭게 바꾸어 사용할 수 있는 DB 프로그램을 작성하는 데는 두 가지 문제가 있다.
첫번째 문제 : JDBC 코드에서 사용하는 SQL
대부분의 DB는 표준을 따르지 않는 비표준 문법과 기능도 제공하고, 이런 비표준 특정 DB 전용 문법은 매우 넓게 사용되고 있다. 해당 DB의 특별한 기능을 사용하거나 최적화된 SQL을 만들 때 유용하기 때문이다.
특별한 기능을 제공하는 함수를 SQL에 사용하려면 대부분 비표준 SQL 문장이 만들어진다. 이렇게 작성된 비표준 SQL은 결국 DAO 코드에 들어가고, 해당 DAO는 특정 DB에 대해 종속적인 코드가 된다. 보통은 DB가 자주 변경되지도 않고, 사용하는 DB에 최적화하는 것이 중요하므로 비표준 SQL을 거리낌없이 사용한다.
해결책
두번째 문제 : SQLException
JDBC API는 SQLException
만 던지지만, DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이다. 예외가 발생한 원인은 SQLException
안에 담긴 에러 코드와 SQL 상태정보를 참조해봐야 한다. 그런데 DB 벤더마다 정의한 고유한 에러 코드를 사용하기 때문에 DB 에러 코드또한 DB별로 모두 다르다.
그래서 SQLException
은 예외가 발생했을 때의 DB 상태를 담은 SQL 상태정보를 부가적으로 제공한다. 예를 들어 테이블이 존재하지 않는 경우 42S02와 같은 식이다. 이 상태정보는 DB에 독립적이다.
그런데 문제는 DB의 JDBC 드라이버에서 SQLException
을 담을 상태 코드를 정확하게 만들어주지 않는다는 점이다.
해결책 : 없음
호환성 없는 에러 코드와 표준을 잘 따르지 않는 상태 코드를 가진 SQLException
만으로 DB에 독립적인 유연한 코드를 작성하는 건 불가능하다.
SQL 상태 코드는 JDBC 드라이버를 만들 때 들어가는 것이므로 같은 DB라고 하더라도 드라이버를 만들 때마다 달라지기도 하지만, DB 에러 코드는 DB에서 직접 제공해주는 것이니 버전이 올라가더라도 어느 정도 일관성이 유지된다.
해결 방법은 DB별 에러 코드를 참고해서 발생한 예외의 원인이 무엇인지 해석해주는 기능을 만드는 것이다. 스프링은 데이터 액세스 작업 중에 발생할 수 있는 예외상황을 수십 가지 예외로 분류하고 이를 추상화해 정의한 다양한 예외 클래스를 제공한다.
문제는 DB마다 에러 코드가 다르다는 것이다. 대신 스프링은 DB별 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 매핑해놓은 에러 코드 매핑정보 테이블을 만들어두고 이를 이용한다.
드라이버나 DB 메타정보를 참고해서 DB 종류를 확인하고 DB별로 미리 준비된 매핑정보를 참고해서 적절한 예외 클래스를 선택하기 때문에 DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있는 것이다. 그래서 JdbcTemplate
을 이용하면 JDBC에서 발생하는 DB 관련 예외는 거의 신경 쓰지 않아도 된다.
그런데 애플리케이션에서 직접 정의한 예외를 발생시키고 싶을 수 있다. 예를 들어, 애플리케이션 레벨의 체크 예외인 DuplicateUserIdException
을 던지게 하고 싶다면 스프링의 DuplicationKeyException
예외를 전환해주는 코드를 DAO 안에 넣으면 된다.
JDBC 4.0부터는 기존에 JDBC의 단일 예외 클래스였던 SQLException
을 스프링의 DataAccessException
과 비슷한 방식으로 좀 더 세분화해서 정의하고 있다. 하지만 SQLException
의 서브클래스이므로 여전히 체크 예외라는 점과 그 예외를 세분화하는 기준이 SQL 상태정보를 이용한다는 점이 여전히 문제다. 그래서 아직은 스프링의 에러 코드 매핑을 통한 DataAccessException
방식을 사용하는 것이 이상적이다.
DataAccessException
계층구조자바는 JDBC 외에도 데이터 액세스를 위한 표준 기술인 JDO, JPA 등이 존재한다.DataAccessException
은 JDBC 외의 자바 데이터 액세스 기술에서 발생하는 예외에도 적용된다.DataAccessException
은 의미가 같은 예외라면 데이터 액세스 기술의 종류와 상관없이 일관된 예외가 발생하도록 만들어준다. 데이터 액세스 기술에 독립적인 추상화된 예외를 제공하는 것이다.
📌 DAO를 따로 사용하는 이유
- 데이터 액세스 로직을 담은 코드를 성격이 다른 코드에서 분리해놓기 위해서
- 분리된 DAO는 전략 패턴을 적용해 구현 방법을 변경해서 사용할 수 있게 만들기 위해서
DAO를 사용하는 쪽에서는 DAO가 내부에서 어떤 액세스 기술을 사용하는지 신경 쓰지 않아도 된다. 그런 면에서 DAO는 인터페이스를 사용해 구체적인 클래스 정보와 구현 방법을 감추고, DI를 통해 제공되도록 만드는 것이 바람직하다.
그런데 DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해서 DAO를 사용하는 클라이언트에게 감출 수 있지만, 메서드 선언에 나타나는 예외정보가 문제가 될 수 있다.
예를 들어, UserDao
의 인터페이스를 분리해서 기술에 독립적인 인터페이스로 만들려면 다음과 같이 정의해야하지만, JDBC API를 사용하는 UserDao
구현 클래스의 add()
메서드라면 SQLException
을 던지기 때문에 인터페이스 메서드에도 throws가 있어야 한다.
public interface UserDao {
public void add(User user);
이렇게 정의한 인터페이스는 JDBC가 아닌 데이터 액세스 기술로 DAO 구현을 전환하면 사용할 수 없다. 데이터 액세스 기술의 API는 자신만의 독자적인 예외를 던지기 때문이다. 다행히도 JDBC 이후에 등장한 JDO, Hibernate, JPA 등의 기술은 런타임 예외를 사용한다. 그래서 JDBC API를 직접 사용하는 DAO의 경우엔 DAO 메서드 내에서 런타임 예외로 포장해기만 하면 처음 선언했던 대로 해도 된다.
대부분의 데이터 액세스 예외는 애플리케이션에서는 복구 불가능하거나 할 필요가 없지만, 비즈니스 로직에서 의미 있게 처리할 수 있는 예외도 있다. 애플리케이션에서는 사용하지 않더라도 시스템 레벨에서 데이터 액세스 예외를 의미 있게 분류할 필요도 있다. 문제는 데이터 액세스 기술이 달라지면 같은 상황에서도 다른 종류의 예외가 던져진다는 점이다. 따라서 DAO를 사용하는 클라이언트 입장에서는 DAO의 사용 기술에 따라서 예외 처리 방법이 달라져야 한다. 결국 클라이언트가 DAO의 기술에 의존적이 될 수밖에 없다.
DataAccessException
계층구조그래서 스프링은 자바의 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException
계층구조 안에 정리해놓았다.
DataAccessException
은 자바의 주요 데이터 액세스 기술에서 발생할 수 있는 대부분의 예외를 추상화하고 있다. 데이터 액세스 기술에 상관없이 공통적인 예외도 있지만 일부 기술에서만 발생하는 예외도 있다. 스프링의 DataAccessException
은 이런 일부 기술에서만 공통적으로 나타나는 예외를 포함해서 데이터 액세스 기술에서 발생 가능한 대부분의 예외를 계층구조로 분류해놓았다.
InvalidDataAccessResourceUsageException
BadSqlGrammarException
HibernateQueryException
또는 TypeMismatchDataAccessException
InvalidDataAccessResourceUsageException
타입의 예외로 던져주므로 시스템 레벨의 예외처리 자겁을 통해 개발자에게 빠르게 통보해주도록 만들 수 있다. ObjectOptimisticLockingFailureException
낙관적인 락킹
같은 정보를 두 명 이상의 사용자가 동시에 조회하고 순차적으로 업데이트를 할 때, 뒤늦게 업데이트한 것이 먼저 업데이트한 것을 덮어쓰지 않도록 막아주는 데 쓸 수 있는 편리한 기능이다.
JDO, JPA, Hibernate처럼 오브젝트/엔티티 단위로 정보를 업데이트하는 경우 발생한다.
이런 예외들은 사용자에게 적절한 안내 메시지를 보여주고, 다시 시도할 수 있도록 해줘야 한다.
스프링의 예외 전환 방법을 적용하면 기술에 상관없이 DataAccessException
의 서브클래스인 ObjectOptimisticLockingFailureException
으로 통일시킬 수 있다.
ORM 기술이 아닌 JDBC 등을 이용해 직접 낙관적인 락킹 기능을 구현한 경우
OptimisticLockingFailureException
을 상속해서 정의해 사용할 수도 있다.기술에 상관없이 낙관적인 락킹이 발생했을 때 일관된 방식으로 예외처리를 해주려면 OptimisticLockingFailureException
을 잡도록 만들면 된다.
따라서 JdbcTemplate
과 같이 스프링의 데이터 액세스 지원 기술을 이용해 DAO를 만들면 사용 기술에 독립적인 일관성 있는 예외를 던질 수 있다. 결국 인터페이스 사용, 런타임 예외 전환과 함께 DataAccessException
예외 추상화를 적용하면 데이터 액세스 기술과 구현 방법에 독립적인 이상적인 DAO를 만들 수 있다.
UserDao
만들기UserDao
클래스를 인터페이스와 구현으로 분리해보자.
📌 인터페이스 구분 방법
- 인터페이스 이름 앞에 I라는 접두어 붙이는 방법
- 인터페이스 이름은 가장 단순하게 하고 구현 클래스는 각각의 특징을 따르는 이름을 붙이는 방법
UserDao
의 setDataSource()
메서드는 인터페이스에 추가하면 안된다. setDataSource()
메서드는 UserDao
의 구현 방법에 따라 변경될 수 있는 메서드이다. UserDao
를 사용하는 클라이언트가 알고 있을 필요도 없다. UserDao
, 구현 클래스 이름 : UserDaoJdbc
UserDao
-> UserDaoJdbc
UserDao
를 상속받은 UserDaoJdbc
public class UserDaoJdbc implements UserDao {
UserDao
인스턴스 변수 선언은 UserDaoJdbc
로 변경할 필요가 없다.
@Autowired
는 스프링의 컨텍스트 내에서 정의된 빈 중에서 인스턴스 변수에 주입 가능한 타입의 빈을 찾아주기 때문이다. UserDaoJdbc
오브젝트는 UserDao
타입이다. UserDaoJdbc dao
라고 선언할 수도 있다. 중요한 건 테스트의 관심이다. UserDao
인터페이스로 받아서 테스트하는 편이 낫다. 나중에 다른 데이터 액세스 기술로 DAO 빈을 변경한다고 하더라도 이 테스트는 여전히 유효하다.UserDao
의 구현 내용에 관심을 가지고 테스트하려면 테스트에서 @Autowired
로 DI받을 때 UserDaoJdbc
같이 특정 타입을 사용해야 한다. DataAccessException
예외 중의 하나가 던져져야 한다. assertThrows()
를 이용한다. DuplicateKeyException
이 발생한다. DuplicateKeyException
은 아직까지는 JDBC를 이용하는 경우에만 발생한다. SQLException
에 담긴 DB의 에러 코드를 바로 해석하는 JDBC의 경우와 달리 JPA나 Hibernate, JDO 등에서는 각 기술이 재정의한 예외를 가져와 스프링이 최종적으로 DataAccessException
으로 변환하는데, DB의 에러 코드와 달리 이런 예외들은 세분화되어 있지 않기 때문이다.
또한, DataAccessException
이 기술에 상관없이 어느 정도 추상화된 공통 예외로 변환해주긴 하지만 근본적인 한계 때문에 완벽하다고 기대할 수는 없다. 따라서 DataAccessException
을 잡아서 처리하는 코드를 만들려고 한다면 미리 학습 테스트를 만들어서 실제로 전환되는 예외의 종류를 확인해둘 필요가 있다.
만약 DAO에서 사용하는 기술의 종류와 상관없이 동일한 예외를 얻고 싶다면 DuplicatedUserIdException
처럼 직접 예외를 정의해두고, 각 DAO의 add()
메서드에서 좀 더 상세한 예외 전환을 해줄 필요가 있다.
SQLException
을 직접 해석해 DataAccessException
으로 변환하는 코드의 사용법을 살펴보자.
추가 사항
SQLErrorCodeExceptionTranslator
를 사용한다.
스프링에서 SQLException
을 DataAccessException
으로 전환하는 가장 보편적이고 효과적인 방법은 DB 에러 코드를 이용하는 것이다. SQLException
을 코드에서 직접 전환하고 싶다면 SQLExceptionTranslator
인터페이스를 구현한 클래스 중에서 SQLErrorCodeExceptionTranslator
를 사용하면 된다.
UserDaoTest
에 DataSource
변수를 추가해서 DataSource
타입의 빈을 받아두자.
SQLErrorCodeExceptionTranslator
는 에러 코드 변환에 필요한 DB의 종류를 알아내기 위해 현재 연결된 DataSource
를 필요로 한다.
이 DataSource
를 사용해 SQLException
에서 직접 DuplicateKeyException
으로 전환하는 기능을 확인해보는 학습 테스트다.
JdbcTemplate
을 사용하는 UserDao
를 이용해 강제로 DuplicateKeyException
을 발생시킨다.
가져온 DuplicateKeyException
은 중첩된 예외로 JDBC API에서 처음 발생한 SQLException
을 내부에 갖고 있다. getRootCause()
메서드를 이용하면 중첩되어 있는 SQLException
을 가져올 수 있다.
스프링의 예외 전환 API를 직접 적용해 DuplicateKeyException
이 만들어지는지 확인한다.
2-1. 주입받은 DataSource
를 이용해 SQLErrorCodeExceptionTranslator
의 오브젝트를 만든다.
2-2. 거기에 SQLException
을 파라미터로 넣어서 translate()
메서드를 호출해주면 SQLException
을 DataAccessException
타입의 예외로 변환해준다.
2-3. 변환된 DataAccessException
타입의 예외가 정확히 DuplicateKeyException
타입인지 확인한다.
📌
assertThat().isInstanceOf()
뒤에 클래스를 넣으면 오브젝트의equals()
비교 대신 주어진 클래스의 인스턴스인지 검사해준다.
- 책에선
assertThat()
의is()
메서드에 클래스를 넣는다.
JDBC 외의 기술을 사용할 때도 DuplicateKeyException
을 발생시키려면 SQLException
을 가져와서 직접 예외 전환을 하는 방법을 생각해볼 수 있다. 또는 JDBC를 이용하지만 JdbcTemplate
과 같이 자동으로 예외를 전환해주는 스프링의 기능을 사용할 수 없는 경우라도 SQLException
을 그대로 두거나 의미 없는 RuntimeException
으로 뭉뚱그려서 던지는 대신 스프링의 DataAccessException
계층의 예외로 전환하게 할 수 있다.
4장에선 엔터프라이즈 애플리케이션에서 사용할 수 있는 바람직한 예외처리 방법을 살펴봤다. 또한 JDBC 예외의 단점이 무엇인지 살펴보고, 스프링이 제공하는 효과적인 데이터 액세스 기술의 예외처리 전략과 기능에 대해서도 알아봤다.
throws
선언을 남발하는 것은 위험하다.catch
/throws
를 피하기 위해 런타임 예외로 포장하는 두 가지 방법의 예외 전환이 있다.SQLException
은 대부분 복구할 수 없는 예외이므로 런타임 예외로 포장해야 한다.SQLException
의 에러 코드는 DB에 종속되기 때문에 DB에 독립적인 예외로 전환될 필요가 있다.DataAccessException
을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 계층을 제공한다.