public void deleteAll() throws SQLException{...}
이 아래와 같이 변경
public void deleteAll(){
this.jdbcTemplate.update("delete from users");//???
}
try{
...
}
catch(SQLException e){
// nothing. 특수한 상황이 아니라면 절대로 이런식으로 처리해서는 안된다.
}
(예외를 무시하거나 잡아먹어 버리는 코드는 만들면 안된다.)
public void method1() throws Exception {
method2();
...
}
public void method2() throws Exception {
method3()
...
}
public void method3() throws Exception {
...
}
시스템에 비정상적인 상황이 발생( JVM에서 발생. OOM이나 ThreadDeath... )
App에서 문제를 잡으려고 하면 안된다.
아무런 대응방법이 없기 때문에 신경쓰지말자.
개발자가 만든 App에서 예외상황이 발생했을 경우에 사용
문제를 해결해서 정상 상태로 돌려놓음
Ex)
User가 요청한 파일을 읽으려고 시도했는데 해당 파일이 없다거나 다른 문제가 있어서 읽히지 않아서 IOExceptiondl 발생 ->
사용자에게 상황을 알려주고, 다른 파일을 이용하도록 안내.
(App구동에는 문제x)
Ex)
다른 API 호출에 실패했을 경우, 3회 정도 retry해서 원래 로직을 복구한다.
자신이 아닌, 자신을 호출한 쪽으로 던진다.
(throws로 던지거나, catch로 로그를 기록한 후 던진다.)
예외처리를 회피하려면 반드시 다른 Obj나 메소드가 예외를 대신 처리할 수 있어야 한다.
public void add() throws SQLException{ ... }
// 스프링부트에선 @ControllerAdive를 의미하는 듯.
Ex)
SQLException은 콜백 오브젝트가 아닌 템플릿의 역할이므로,
그쪽으로 예외를 다 떠넘긴다.
예외를 복구해서 정상적인 상태로 만들 수 없기 때문에 예외를 메소드 밖으로 던지지만,
예외 회피와 달리 적절한 예외로 전환해서 던진다.
Ex)
새로운 사용자를 등록시 중복된 ID가 있다면,
SQLException이 아니라, DuplicateUserIdException과 같은 예외를 던지는 편이 낫다.
그리고 중첩예외로 처리하는 편이 낫다.
(예외 원인의 근본을 확인할 수 있음.)
catch(SQLException e){
...
throw DuplicateUserIdException(e);
}
중첩 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부에 담아서 던진다.
( 주로 체크 예외를 언체크 예외인 런타임 예외로 바꾸는데 사용. )
참고로 API가 던지는 예외가 아닌 경우 체크 예외를 사용하는 것이 적잘하다.
(비즈니스적인 의미가 있는 예외는 적절한 대응 및 복구가 필요하기 때문임 -> RuntimeException으로 하자)
따라서, 복구하지 못할 예외라면 RuntimeException으로 포장해서 던지고, 예외처리 서비스 등을 이용해 자세한 로그를 남기며
관리자에게 메일로 통보하며, 사용자에게는 안내 메시지를 보여주는 식이 적절하다.
체크 예외가 일반적인 예외를 다루고,
언체크 예외는 시스템 장애, 프로그램상의 오류에 사용한다.
체크는 복구가 가능하기 때문에, catch 블록이나 throws선언을 강제하고 있다.
(체크는 애플리케이션의 종료를 방지해준다.)
스프링부트의 경우 예외가 발생했다고 작업을 일시 중지해서 예외상황을 복구할 수 있는 방법이 없다.
(그래서, 해당 요청의 작업을 취소하고, 관리자에게 통보를 한다.)
따라서, 런타임 예외로 전환해서 던지는 게 낫다.
public void add() throws SQLException, DuplicatedUserIdException{...}
SQLException은 복구 불가능한 예외이므로 처리할 수가 없다.
따라서 런타임 예외로 포장해서 처리하는 편이 훨씬 낫다.
public class DuplicateUserIdException extends RuntimeException{
public DupicateUserIdException(Throwable cause){
super(cause);
}
}
public void add() throws DuplicateUserIdException{
try{
...
}
catch(SQLException e){
if(e.getErrorCode() == ...)
throw new DuplicateuserIdException(e);// 예외 전환
else
throw new RuntimeException(e);// UpCasting을 통한 예외 포장
}
}
런타임 예외 중심의 전략은 낙관적인 예외처리 기법이다.
(복구할 수 있는 예외는 없다고 가정하고, 어차피 예외발생해도 시스템 레벨에서 알아서 처리해 줄 것이고,
꼭 필요한 경우는 복구하거나 대응처리하면 된다.)
그래서 다음과 같이 처리
state에 따라서 다른 결과값을 반환한다.
(정상적인 출금처리와 잔고 부족이 발생했을 경우 두가지를 분리)
또한 반드시, 일관된 예외상황에서의 결과값에 대한 정책을 완벽하게 수립해야만 한다.
정상적인 흐름을 따르는 코드는 유지하고, 예외 상황에서는 비즈니스적인 의미를 띈 예외를 던지도록 한다.
(잔고 부족인 경우, InsufficientBalanceException 발생)
추가로 예외는 체크 조건으로 하는 것이 좋다.
try{
BigDecimal balance = account.withdraw(amount);
...
// 정상적인 처리 결과를 출력하도록 진행
}
catch(InsufficientBalanceException e){
// 체크 예외
// InsufficientBalanceException에 담긴 인출 가능한 잔고금액 정보를 가져옴.
BigDecimal availFunds = e.getAvailFunds();
...
// 잔고 부족 안내 메시지를 준비하고 이를 출력하도록 진행
}
99% SQLException은 코드 레벨에서 복구할 수가 없다.
따라서, 관리자에게 빨리 예외가 발생했다는 사실을 전달하는 것 외에는 없다.
따라서 언체크/런타임 예외로 전환해줘야 한다.
스프링의 JdbcTemplate은 콜백 안에서 발생하는 모든 SQLException을 런타임 예외인 DataAccessException으로 포장해서 던진다.
]
추가 스프링의 API 메소드는 대부분 런타임 예외이므로,이를 강제로 처리할 필요가 없다.
catch/throws를 줄여주는 것 + 로우레벨의 예외를 좀 더 의미 있고 추상화가 되도록
자바를 이용해 DB에 접근하는 방법을 추상화된 API형태로 정의해놓고,
각 DB업체가 JDBC 표준을 따라 만들어진 드라이버를 제공해준다.
그러나 DB 변경에 유여한 코드를 작성하기 힘들다.
대부분의 DB는 표준을 따르지 않는 비표준 문법과 기능도 제공한다.
비표준 SQL은 결국 DB에 종속적이게 된다.
(따라서, 변경이 발생하지 않는 상황에 적합하다.)
결국 현실적으로 DAO를 DB별로 사용하거나, SQL을 외부에서 독립시켜서 바꿔 쓸 수 있게 하는 것이다.
DB마다 SQL만 다른 것이 아니라 에러의 종류와 원인도 제각각이라는 점이다.
그래서 JDBC는 다양한 에러를 SQLException으로 하나로 묶는다.
이 문제를 해결하기 위해서 SQLException은 상태 코드를 통해서 구분지었다.
(그런데, 여전히 문제 많음)
SQL 상태 코드는 신뢰할 만하지 않다.
(차라리 업체별 DB 전용 에러 코드가 더 정확함.)
따라서, DB별 에러 코드를 참고해서 예외의 원인이 무엇인지 해석해주는 기능을 만드는 것이다.
Ex) 키 값이 중복된 경우
MySQL -> 1062
오라클 -> 1
DB2 -> -803
스프링은 DB별 에러 코드를 분류해서 스프링이 정의한 예외 클래스와 에러 코드 매핑정보 테이블을 만들어두고 이를 이용한다.
(DB별로 미리 준비된 에러 코드와 비교해서 적절한 예외를 발생시킨다.)