📚 본 글은 토비의 스프링 3.1을 읽고 정리한 글입니다.
예외는 반시드 처리되어야 한다.
catch블록
을 이용해 화면에 메세지를 출력하는 것은 예외를 처리한 것이 아니다.
모든 예외는 적절하게 복구되든지, 작업을 중단시키고 운영자 또는 개발자에게 통보되어야 한다.
어떠한 경우에도 다음 두가지의 경우는 피해야 한다.
- 예외처리를 하지 않는 경우
throws Exception
을 습관처럼 붙이는 경우
자바에서는 throw
를 통해 발생시킬 수 있는 예외는 크게 세 가지가 있다.
java.long.Error
의 서브클래스- 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용
- 주로 JVM 에서 발생시키는 것으로 애플리케이션 코드로 잡으려하면 안됨
- 🤷♂️ why?
catch
로 잡아봤자 대응 방법이 없음
- 🤷♂️ why?
java.lang.Exception
클래스와 그 서브 클래스- 애플리케이션 코드 작업 중에 예외 상황이 발생했을 경우에 사용
- RuntimeException을 상속하지 않은 것을 체크 예외라고 부름
- 체크 예외가 발생할 수 있는 메소드 사용 시 반드시 예외를 처리하는 코드도 함께 작성되어야 함
catch
또는throws
를 통해 처리가 되지 않으면 컴파일 에러가 발생
java.lang.RuntimeException
클래스의 서브 클래스java.lang.RuntimeException
를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 에외라고 불림catch
로 잡거나throws
로 선언하지 않아도 됨- 주로 프로그램상 오류가 있을 때 발생하도록 의도된 것
- ex)
NullPointException
,IllegalArgumentException
- 예외 상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것
- 예외로 인해 기본 작업 흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도해주는 것도 예외상황은 다시 정상으로 돌아오고 예외를 복구했다고 볼 수 있다.
- 예외가 처리됐다면 비록 기능적으로는 사용자에게 예외상황으로 비쳐도 애플리케이션에서는 정상적으로 설계된 흐름을 따라 진행되어야 한다.
- 재시도가 의미있는 경우라면 반복적으로 시도함으로써 예외상황에서 복구되게 할 수 있다.
int maxRetry = MAX_RETRY;
while(maxRetry -> 0) {
try {
... // 예외가 발생할 가능성이 있는 시도
return; // 작업 성공
}
catch (SomeException e) {
// 로그 출력. 정해진 시간만큼 대기
}
finally {
// 리소스 반납. 정리 작업
}
throw new RetryFailException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
}
- 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것
- 예외를 자신이 처리하지 않고 회피하는 방법
- 예외를 회피하려면 반드시 다른 오브젝트나 메소드가 예외를 대신 처리할 수 있도록 던져줘야 한다. (다음 예제코드 참고)
- 예외를 회피하는 것은 예외를 복구하는 것처럼 의도가 분명해야 한다.
- 자신을 사용하는 쪽에서 예외를 다루는 게 최선의 방법이라는 분명한 확신이 있어야 한다.
// 예외처리 회피 1
public void add() throws SQLException {
// JDBC API
}
// 예외처리 회피 2
public void add() throws SQLException {
try {
// JDBC API
}
catch (SQLException e) {
// 로그 출력
throw e;
}
}
- 발생한 예외를 그대로 넘기는 것이 아니라 적절한 예외로 전환해서 던지는 것이다. 예외 회피와 비슷하게 예외를 복구해서 정상적인 상태로는 만들 수없기 떄문에 예외를 메소드 밖으로 던지는 것이다. 예외를 전환해서 던진다는 특징이 있다.
- 예외 전환은 두 가지 목적으로 사용됨
- 내부에서 발생한 예외를 그대로 던지는 것이 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우
- 예외를 처리하기 쉽고 단순하게 만들기 위해 포장(wrap) 하는 것
- 주로 예외처리를 강제하는 체크예외를 언체크 예외인 런타임 예외로 바꾸는 경우가 이에 속한다. 예제코드
// 1. 적절한 의미를 부여해주지 못하는 경우
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 그대로
}
}
// 2. 예외 포장
try {
OrderHome orderHome = EJBHomeFactory.getInstanse().getorderHome();
Order order = orderHome.fineByPrimaryKey(Integer id);
} catch (NamingException ne) { // 체크예외
throw new EJBException(ne); // 언체크 예외로 던짐
} catch (SQLException se) { // 체크예외
throw new EJBException(se); // 언체크 예외로 던짐
} catch (RemoteException re) {
throw new EJBException(re);
}
RuntimeException을 상속받지 않은 체크예외가 일반적인 예외를 다루고, 시스템 장애 또는 프로그램상의 오류를 언체크 예외가 사용된다.
예외를 처리하는 가장 좋은 방법은 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는 것이다. 또는 프로그램의 오류나 외부 환경으로 인해 예외가 발생하는 경우라면 빨리 해당 요청을 서버관리자 또는 개발자에게 통보해주는 것이 좋다.
하지만, 점차 자바의 환경이 서버로 이동하면서 체크예외의 활용도와 가치는 점점 떨어지고 있다. 그러므로 대응이 불가능한 체크 예외들은 빨리 런타임 예외로 전환해서 던지는 것이 낫다.
시스템 또는 외부의 예외상황이 원인이 아니라 애플리케이션 자체의 로직에 의해 의도적으로 발생시키고, 반드시 catch 해서 무엇인가 조취를 취하도록 요구하는 예외를 말한다.
예를들면, 사용자가 요청한 금액을 은행계좌에서 출금하는 기능을 가진 메소드가 있다. 현재잔고를 확인하고 허용하는 범위를 넘어서 출금을 요청하면 출금작업을 중단시키고, 적절한 경고를 사용자에게 보내는 기능을 가진 메소드를 설계하는 방법은 두 가지가 있다.
- 정상적인 출금 처리를 했을 경우와 잔고부족이 발생했을 경우 각각 리턴타입을 다르게 돌려준다.
- 이 경우에는 이 메소드를 호출한 쪽에서 리턴타입을 반드시 확인하여 if-else 처리를 해주어야 하므로 코드가 복잡해지고 불편할 수 있다.
- 정상적인 흐름을 따르는 코드는 그대로 두고, 잔고 부족과 같은 예외 상황에서는 비즈니스적인 의미를 띈 예외를 던지도록 설계한다.
- 정상적인 흐름을 따르지만 예외가 발생할 수 있는 코드를 try 블록 안에 모아두고 , 예외상황 처리는 catch 블록에 모아둘 수 있기 때문에 코드를 이해하기가 편하다.
- 이때 사용하는 예외는 의도적으로 체크예외로 만듦으로써 예외상황에 대한 로직을 반드시 만들어주게끔 강제하는 것이 좋다.
// 2번에 대한 예제 코드
try {
BigDecimal balance = account.withdraw(amount);
...
// 정상적인 처리 결과를 출력하도록 진행
}
catch (InsufficientBalaceException r) { // 체크 예외
// InsufficientBalaceException에 담긴 인출 가능한 잔고금액 정보를 가져옴
BigDecimal avaliFunds = e.getAvailFunds();
...
// 잔고 부족 안내 메세지를 준비하고 이를 출력하도록 진행
}
- 예외를 잡아서 아무런 조취도 취하지 않거나 의미 없는 throws 선언을 남발하는 것은 위험하다.
- 예외는 복구하거나 예외처리 오브젝트로 의도적으로 전달하거나, 적절한 예외로 전환해야 한다.
- 좀 더 의미 있는 예외로 변경하거나, 불필요한 catch/throws 를 피하기 위해 런타임 예외로 포장하는 두 가지 방법의 예외 전환이 있다.
- 복구할 수 없는 예외는 가능한 한 빨리 런타임 예외로 전환하는 것이 바람직하다.
- 애플리케이션의 로직을 담기 위한 예외는 체크 예외로 만든다.
- JDBC의
SQLException
은 대부분 복구할 수 없기 때문에 런타임 예외로 포장해야 한다. - 스프링은
DataAccessException
을 통해 DB에 독립적으로 적용 가능한 추상화된 런타임 예외 계층을 제공한다.