✅ 초난감 예외처리
- UserDao, deleteAll()
//== 테이블 전체 데이터 삭제 ==// public void deleteAll() { String sql = "DELETE FROM users"; //콜백 객체 생성을 내장 함수가 담당한다. this.jdbcTemplate.update(sql); }
deleteAll()
메소드를 보면 기존의 throws SQLException 키워드가 사라진 것을 볼 수 있다. catch로 잡은 예외는 어디로 가는 것일까?
예외가 발생하면 그것을 catch 블록을 써서 잡아내는 것까지는 좋은데 그리고 아무것도 하지 않고 넘어가 버리는 건 정말 위험한 일이다. 프로그램 실행 중에 발생한 오류를 무시하고 계속 진행해버리기 때문이다.
- 잘못된 오류 처리(1) : 예외 블랙홀
} catch (SQLException e) { System.out.println(e); }
} catch (SQLException e) { e.printStackTrace(); }
개발 중에는 위와 같이 IDE 콘솔이나 실행창에 이 메시지가 눈에 띄어서 바로 조치할 수 있을지 모르겠다. 하지만 다른 로그나 메시지에 금방 묻혀 버리고 말 것이다. 위와 같이 단순히
try-catch
블럭만 사용하는 것은 오류 처리가 아니다.모든 예외는 적절하게 복구되든지 아니면 작업을 중단시키고 운영자 또는 개발자에게 분명하게 통보돼야 한다.
- 잘못된 오류 처리(2) : 무의미하고 무책임한 throws
위와 같이 메소드 선언부에 기계적으로 throws 키워드를 사용하는 개발자도 있다. 하지만 이를 통해 의미있는 정보를 얻을 방법이 없다. 정말 예외적인 상황이 발생한 것인지, 습관적으로 붙여넣은 건지도 구별하기 어렵다.
✅ UnChecked, Checked
👉 자세한 내용은 위의 링크를 참고하시기 바랍니다.
일반적으로 예외라고 하면 Exception 클래스의 서브클래스 중에 RuntimeException을 상속하지 않은 체크 예외(Checked Exception)을 말한다. 체크 예외가 발생할 수 있는 메소드를 사용할 때는 반드시 catch 문으로 잡거나 throws 키워드를 정의해서 호출한 곳으로 오류를 던져야 한다.
✅ 예외 처리 방법
- 예외 복구
첫 번째 예외처리 방법은 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것이다. 예를 들어 사용자가 요청한 파일을 읽으려고 시도했는데 해당 파일이 없다거나 다른 문제가 있어서 읽히지가 않아서 IOException이 발생한 경우, 기본 작업 흐름이 불가하다면 다른 작업 흐름으로 자연스럽게 유도해 주는 것이다. 이런 경우를 예외를 복구했다고 볼 수 있다. 단순히 에러 메시지를 사용자에게 던지는 것은 예외 복구가 아니다.
int maxretry = MAX_RETRY; while(maxretry — > 0) { try { ... // 예외가 발생할 가능성이 있는 시도 return; // 작업 성공 } catch(SomeException e) { // 로그 출력. 정해진 시간만큼 대기 } finally { // 리소스 반납. 정리 작업 } } throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
통제 불가능한 외부 요인으로 인해 예외가 발생할 경우, 최대 횟수만큼 반복적으로 다시 시도하여 예외 상황이 복구되게 위와 같이 코드를 구성할 수 있다.
- 예외 처리 회피
두 번째 방법은 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다. throws 키워드로 선언해서 예외가 발생하면 알아서 던져지게 하거나, catch 문으로 예외를 잡은 후 다시 throw 키워드로 예외를 생성해서 던지는 것이다.
public void add() throws SQLException { // JDBC API } public void add() throws SQLException { try { // JDBC API } catch (SQLException) { // 로그 출력 throw e; } }
👉 JdbcTemplate이 사용하는 콜백 오브젝트는 SQLException을 자신이 처리하지 않고 템플릿으로 던진다. 콜백 오브젝트의 메소드는 예외를 처리하는 역할이 아니라고 보기 때문이다. 하지만 자신의 코드에서 발생하는 예외를 그냥 던지는 것은 무책임한 책임회피일 수 있다. 만약 DAO에서 계속 throws 키워드를 통해 오류를 던진다면, Service 계층이나 Controller에서 해당 오류를 처리할 수 있을까? 결국 처리하지 못하고 서버로 다시 던져지게 될 것이다. 따라서 예외를 회피하는 것은, 예외 처리의 관계가 명확한 템플릿/콜백 패턴이 적용된 콜백 오브젝트처럼 의도가 명확해야 한다.
✅ 예외 전환
예외 전환도 예외를 복구해서 정상적인 상태로 만들 수 없기 때문에 예외를 메소드 밖으로 던진다. 하지만 예외 회피와 달리, 발생한 예외를 그대로 넘기는 게 아니라 적절한 예외로 전환해서 던진다.
➡️ 예외는 두 가지 목적으로 사용된다.
- 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서
내부에서 발생한 예외를 그대로 던지는 것이 그 예외 상황에 대한 적절한 의미를 부여해주지 못하는 경우, 의미를 분명하게 해주는 다른 예외로 변경해서 던지는 것이다. 예를 들어, ID가 같은 사용자를 쿼리문으로 DB에 저장하려고 하면 SQLException이 발생한다. 하지만, 오류가 발생한 SQLException을 그대로 호출한 곳으로 던지면 어떤 쿼리 때문에 발생했는지 알 수가 없다. 기능에 따른 수많은 쿼리문이 존재할 것이기 때문이다.
그래서 DAO에서 SQLException의 정보를 해석해서 DuplicateUserIdException 같은 예외로 변경해서 던져준다면, 의미가 분명한 예외를 통해 서비스 계층 오브젝트에 대하여 적절한 복구 작업을 시도할 수가 있다.
public void add(User user) throws DuplicateUserIdException, SQLException { try { // JDBC를 이용해 user 정보를 DB에 추가하는 코드 또는 // 그런 기능을 가진 다른 SQLException을 던지는 메소드를 호출하는 코드 } catch(SQLException e) { // ErrorCode가 MySQL의 "Duplicate Entry(1062)"이면 예외 전환 if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) throw DuplicateUserIdException(); else throw e; // 그 외의 경우는 SQLException 그대로 } }
- 예외 처리를 단순하게 하기 위한 포장(wrap) 방법
중첩 예외를 이용해 새로운 예외를 만들고 원인이 되는 예외를 내부에 담아서 던지는 방식이다. 의미를 명확하게 하려고 다른 예외로 전환하는 것이 아닌, 예외 처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다.
try { OrderHome orderHome = EJBHomeFactory.getInstance().getOrderHome(); Order order = orderHome.findByPrimaryKey(Integer id); } catch (NamingException ne) { throw new EJBException(ne); } catch (SQLException se) { throw new EJBException(se); } catch (RemoteException re) { throw new EJBException(re); }
EJBException은 RuntimeException 클래스를 상속한 런타임 예외(UnChecked)이다. 이렇게 런타임 예외로 만들어서 전달하면 이를 시스템 Exception으로 인식하고 트랜잭션을 자동으로 롤백해준다. 런타인 예외이기 때문에 EJB를 사용하는 다른 클래스에서 일일이 예외를 잡거나 던지는 수고를 덜 수도 있다.
✅ 예외 처리 전략
🔹런타임 예외(UnChecked Exception)의 보편화
자바 엔터프라이즈 서버 환경은 수많은 사용자가 동시에 요청을 보내고 각 요청이 독립적인 하나의 작업으로 취급된다. 하나의 요청을 처리하는 중에 예외가 발생하면 해당 작업만 중단시키고, 다른 작업들은 그대로 진행하여 전체 시스템의 중단을 예방하는 것이 좋다. 작업을 중지 시킨 후 사용자와 커뮤니케이션 하면서 예외 상황을 바로 복구할 수 있는 방법이 없기 때문이다.
👉 최근에 등장하는 표준 스펙 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하여, API 차원에서 런타임 예외를 던지도록 만드는 것이 추세이다.
🔹add() 메소드 예외 처리
add()
메소드는 SQL문을 실행하는 PreparedStatement가 있기 때문에 SQLException이 발생할 수 있다. 또 DB에 Insert할 때 ID가 중복일 수 있으므로 DuplicateUserIdException이 발생할 수도 있다. DuplicatedUserIdException은 충분히 복구가 가능한 오류이다. 언제든지 복구 가능한 오류라면 체크 예외로 만들지 않고 런타임 예외로 만들어 컴파일 시점에서 오류를 내는 것이 아니라 런타임 시에 오류를 내도록 하여 신경 쓰지 않도록 해주는 것이 좋다.
대신 명시적으로 해당 메소드가 DuplicateUserIdException을 던진다고 명시하는 것이 좋겠다.
👉 아래는 예시이다. 현재 우리는 JdbcTemplate으로 구성했기 때문에 아래 코드를 적용하려면 많이 수정해야 되기 때문에 간단히 훑고 넘어가자.
- DuplicateUserIdException
public class DuplicateUserIdException extends RuntimeException { public DuplicateUserIdException(Throwable cause) { super(cause); } }
- UserDao, add()
public static void add() throws DuplicateUserIdException{ try{ // 1. 회원가입 로직 ... // 2. ID, PW 정보 DB INSERT 시 ID 중복(무결정 제약조건 위배)되어 SQLException 발생 }catch(SQLException e){ if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY){ throw new DuplicateUserIdException(e); // 예외 전환 }else{ throw new RuntimeException(e); // 예외 포장 } } }
🔹애플리케이션 예외
시스템 또는 외부의 예외상황이 원인이 아니라 애프릴케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch
해서 무엇인가 조치를 취하도록 요구하는 예외를 애플리케이션 예외라고 한다. 책에서는 은행계좌의 잔고가 없을 때 출금을 요청하면, 해당 작업을 종료 시키면서 어떻게 그 예외를 처리할지 이야기 하고 있다. 애플리케이션 예외를 처리하는 방법은 두 가지가 있다.
- 리턴 값으로 결과를 확인하고 예외를 체크한다.
- 예외 상황에서 비즈니스적인 의미를 띤 예외를 던지도록 만든다.
try{
BigDecimal balance = account.withdraw(amount);
// 정상적인 처리 결과를 출력하도록 진행
}catch (InsufficientBalanceException e){ // Checked Exception
// InsufficientBalanceException에 잔고금액 정보를 가져옴
BigDecimal availFunds = e.getAvailFunds();
// 잔고부족 안내 메시지를 준비하고 이를 출력하도록 진행
}
InsufficientBalanceException을 Checked Exception으로 생성하여 특정 로직을 통해서 발생될 수 있는 예외를 예상하고 해당 예외 처리를 강제하도록 하는 것이다.
🔹SQLException은 어떻게 살아질 수 있었나?
UserDao를 JdbcTemplate를 도입을 하면서 SQLException에 대한 예외 처리 코드인 throws SQLException 구문이 모두 사라졌다. SQLException은 ChekcedException이기 때문에 해당 예외를 반드시 처리해야 컴파일 에러가 발생하지 않는데 이상하다.
이는 JdbcTemplate에서 해당 예외가 런타임 에러로 전환되었기 때문이다. 먼저 살펴볼 점은 SQLException이 복구될 수 있는가?이다. 결론부터 말하자면 대부분의 SQLException은 복구가 불가능 하다. 통제할 수 없는 외부 상황(SQL 문법 오류, 제약조건 위반, DB 서버 다운)등에 의한 오류이기 때문이다. 단지 개발자는 해당 예외를 발생시키지 않기 위해서 검증하고, 또 검증할 수 밖에 없다.
그래서 JdbcTemplate을 제공하는 스프링 API에서는 해당 오류들을 런타임 오류로 던지면서 try-catch
로 예외 처리 코드를 강제하지 않았다. 개발자가 코드로 예외 처리를 한다고 한들, 복구할 수 없는 코드이기 때문이다.
- JdbcTemplate.class, update()
@Override public int update(String sql, @Nullable Object... args) throws DataAccessException { return update(sql, newArgPreparedStatementSetter(args)); }
위는 ctrl + 좌클릭으로 JdbcTemplate 코드를 확인한 것이다. throws DataAccessException을 던지고 있고 이는 런타임 예외이다.
📖 토비 스프링 3.1 -p279~307
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T