- JdbcTemplate을 대표로 하는 스프링의 데이터 엑세스 기능에 담겨 있는 예외 처리와 관련된 접근 방법과 이를 통해 예외를 처리하는 예시를 살펴보자.
- JdbcContext를 JdbcTemplate으로 바꾸면 SQLException이 사라짐
초난감 예외처리
- 예외는 처리되어야 한다!
- 예외를 catch로 잡고 아무것도 하지 않는 것은 원치 않는 예외가 발생하는 것보다 훨씬 더 안좋음
=> 어디선가 오류가 발생해도 무시하고 계속 진행해버리기 때문에 시스템 오류의 원인을 찾기 힘들어짐- 예외 처리 핵심 원칙 : ‘모든 예외는 적절하게 복구되든지 작업을 중단시키고 개발자에게 분명하게 통보되어야 한다’
- 예외를 잡아 뭔가 조치를 취할 방법이 없다면 잡지 말아야 한다
- 메서드에
throws SQLException
을 선언해서 자신을 호출한 코드에 예외처리 책임 전가하기
- 무의미하고 무책임한
throws Exception
: 첫 번째 문제보다는 낫지만, 매우 안 좋은 예외처리 방법
예외의 종류와 특징
- checked exception이라 불리는 명시적인 처리가 필요한 예외를 사용하고 다루는 방법
- 자바에서 throw를 통해 발생시킬 수 있는 예외 3가지
- Error :
java.lang.Error
의 서브 클래스, 시스템에 뭔가 비정상적인 상황이 발생했을 경우 사용- 어플리케이션에서는 이런 에러에 대한 처리는 신경쓰지 않아도 됨
- Exception과 checked Exception :
java.lang.Exception
클래스와 그 서브 클래스, 개발자들이 만든 애플리케이션 코드의 작업 중 예외상황이 발생한 경우 사용- Checked Exception : RuntimeException을 상속하지 않은 것들
- Unchecked Exception : RuntimeException을 상속한 클래스
- 체크 예외가 발생할 수 있는 메서드를 사용할 경우, 반드시 catch문으로 잡든지, 아니면 다시 throws를 정의해서 메서드 밖으로 던져야 함
- RuntimeException과 언체크/런타임 예외 : 주로 프로그램의 오류가 있을 때 발생
- 에러와 마찬가지로 catch문으로 잡거나 throws 선언하지 않아도 됨
- 피할 수 있지만 개발자가 부주의해서 발생할 수 있는 경우에 발생 (ex. NullPointerException, IllegalArgumentException)
- Error :
예외처리 방법
- 예외 복구 : 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것
- 예외로 인해 기본 작업 흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도
- 단, 에러 메시지가 사용자에게 그냥 던져지는 것은 예외복구라고 볼 수 없음
- 예외처리 회피 : 예외처리를 자신을 호출한 쪽으로 던져버리는 것
throws
문으로 선언해서 알아서 던져지게 하거나catch
문으로 일단 예외를 잡은 후 로그를 남기고 다시 던지는 것- JdbcContext나 JdbcTemplate이 사용하는 콜백 오브젝트는 SQLException에 대한 예외를 회피하고 템플릿 레벨에서 처리하도록 던짐
- 예외를 회피하는 것은 의도가 분명해야 함
- 예외 전환 : 예외 회피와 비슷하게 예외를 복구해서 정상 상태로 만들 수 없기 때문에 예외를 메서드 밖으로 던지는 것, but 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던짐
- 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우, 더 분명한 예외로 바꿔주기 위해
ex) DAO에서 SQLException을 잡아 DuplicateUserException과 같은 의미가 분명한 예외로 바꿔서 던지는 경우- 중첩 예외 (nested exception) : 전환하는 예외에 원래 발생한 예외를 담는 것(생성자 or initCause() 이용), getCause()를 이용해 처음 발생한 예외 확인 가능
- 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우, 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것
ex) 비즈니스적 의미가 있는 예외가 아닌 EJB 컴포넌트에서 발생하는 체크예외를 런타임 예외인 EJBException으로 포장해서 던지는 경우- 비즈니스적 의미가 있는 예외는 적절한 대응이나 복구 작업이 필요하기 때문에 체크 예외를 사용하는 것이 좋음
- 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우, 더 분명한 예외로 바꿔주기 위해
예외처리 전략
- 런타임 예외의 보편화
- 대응이 불가능한 체크 예외는 빨리 런타임 예외로 전환해서 던지는 게 낫다
- 예전엔 복구가능성이 조금이라도 있다면 체크 예외로 만들었지만, 지금은 항상 복구가능한 것이 아니라면 일단 언체크 예외로 만듦
- 런타임 예외를 일반화해서 사용하는 것은 여러모로 장점이 많지만, 컴파일러가 예외처리를 강제하지 않으므로 사용에 더 주의를 기울여야 함
- 애플리케이션 예외
- 외부의 예외상황이 아니라 애플리케이션 로직에 의해 의도적으로 발생시키고, 반드시 catch해서 조치를 취해야하는 경우
- 정상요청과 예외상황에 대해 다른 종류의 리턴 값을 돌려주기
- 리턴 값을 일종의 결과 상태를 나타내는 정보로 활용
- 리턴 값을 명확하게 코드화하고 잘 관리하지 않으면 혼란이 생길 수 있음
- 리턴 값을 확인하는 조건문이 자주 등장해 코드의 가독성이 떨어짐
- 정상요청은 그대로 두고 예외상황에 비즈니스 의미를 띤 예외를 던지기
- 예외가 발생할 수 있는 코드를 try 블록 안에 두고 예외처리는 catch블록에 모아두기 때문에 코드 가독성이 좋아짐
- 이 때는 의도적으로 체크 예외로 만들어서 개발자가 잊지 않고 예외상황에 대한 로직을 구현하도록 강제하는 것이 좋음
SQLException의 행방
- SQLException의 99%는 복구할 방법이 없으므로 빨리 개발자에게 예외가 발생했다는 사실이 알려지도록 전달하는 방법밖에 없음 => 가능한 빨리 언체크/런타임 예외로 전환해줘야 함
- 스프링의 JdbcTemplate은 이 예외처리 전략을 따르고 있다! => 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던짐
- DataAccessException은 SQLException에 담긴 다루기 힘든 상세한 예외정보를 의미있고 일관성 있는 예외로 전환해서 추상화해주려는 용도로 쓰이기도 함
JDBC의 한계
- DB종류에 상관없이 사용할 수 있는 데이터 엑세스 코드를 작성하는 것이 어려운 이유?
- 비표준 SQL : 대부분의 DB가 비표준 문법과 기능도 제공하기 때문
- 이런 비표준 SQL이 DAO 코드에 들어가면 특정 DB에 종속적인 코드가 됨
=> 표준 SQL만 사용하거나, DB별 DAO를 만들거나, SQL을 외부에 독립시키는 방법이 있음 (7장)
- 이런 비표준 SQL이 DAO 코드에 들어가면 특정 DB에 종속적인 코드가 됨
- 호환성 없는 SQLException의 DB 에러정보 : DB마다 에러의 종류와 원인도 제각각이기 때문
DB 에러 코드 매핑을 통한 전환
- SQLException에 담긴 SQL 상태 코드는 신뢰할 수 없으므로 어느정도 일관성이 유지되는 DB 에러 코드를 참고하자
- 스프링은 DataAccessException이라는 SQLException을 대체할 런타임 예외를 정의하고 있을 뿐 아니라 DataAccessException의 서브클래스로 세분화된 예외 클래스들을 정의하고 있음
- 하지만, DB마다 에러 코드가 제각각이라는 문제가 있음
=> 스프링은 DB별 에러 코드를 분류해 스프링이 정의한 예외 클래스와 매핑해놓은 여러 코드 매핑정보 테이블을 만들어두고 이용
=> JdbcTemplate은 SQLException을 단지 런타임 예외로 포장하는 것이 아니라 DB의 에러 코드를 DataAccessException 계층구조의 클래스 중 하나로 매핑해줌, DB가 달라져도 같은 종류의 에러라면 동일한 예외를 받을 수 있다!
=> 여전히 체크 예외라는 점과 SQL 상태정보를 이용한다는 아쉬움..
기술에 독립적인 UserDao 만들기
- 인터페이스 사용 + 런타임 예외 전환 + DataAccessException 예외 추상화