try {
...
} catch (Exception e) {
}
예외를 보고도 무시해버리겠다는 태도는 매우 나쁜 태도이다.
특히 자바, 스프링 개발자라면 더더욱 …
한 곳에서 발생한 예외가 캐스케이드되어 다른 곳에서 또 다른 에러를 일으킬 수 있다.
try {
...
} catch (Exception e) {
**System.out.println(e);**
}
try {
...
} catch (Exception e) {
**e.printStackTrace();**
}
예외가 발생했다는 것을 감지할 수는 있겠으나, 다른 디버깅 로그에 묻혀 놓칠 가능성이 크다.
예외를 처리할 때에 반드시 지켜야 할 핵심 원칙은 딱 하나다.
모든 예외는 적절하게 복구되어야 하며,
그게 아니라면 작업을 중단시키고 개발자에게 분명하게 통보되어야 한다.
...
public void method1() throws Exception {
method2();
}
public void method2() throws Exception {
method3();
}
public void method3() throws Exception {
...
}
throws Exception
을 쓰면 해당 예외를 무조건 던지게 된다.
하지만 실행 중 발생할 수 있지만 발생하면 안되는 예외와는 분명 구분되어야 한다.
결과적으로 적절한 처리를 통해 복구될 수 있는 예외 상황도 제대로 다룰 수 있는 기회를 박탈당한다.
이러한 예외처리는 지양되어야 한다.
자바에서 throw를 통해 발생시킬 수 있는 예외에는 크게 세 가지가 있다.
java.lang.Error 클래스의 서브클래스들이다.
주로 시스템에 뭔가 비정상적인 상황이 발생했을 경우에 사용된다. (주로 자바 VM상에서 발생하는 오류)
따라서 애플리케이션 코드 상에서 잡으려고 하면 안 된다.
즉, 시스템 레벨에서 특별한 작업을 하는 게 아니라면 애플리케이션에서 해당 예외에 대해 신경 쓸 필요는 없다.
체크 예외는 명시적으로 처리가 필요한 예외이며,
추가적으로 RuntimeException
을 상속하지 않는 예외이기도 하다.
체크 예외가 발생할 수 있는 메소드를 사용할 경우,
복구가 가능한 예외들이기 때문에 반드시 예외를 처리하는 코드외 함께 작성해야 한다.
→ catch
문으로 예외를 잡거나 throws
로 예외를 자신을 호출한 클래스로 메소드 밖으로 던져서 해결해야 하는데, 이를 해결하지 않으면 컴파일 에러가 발생한다
java.lang.RuntimeException 클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않으므로 언체크 예외나 런타임 예외라고도 한다.
이들 역시 Error와 마찬가지로 catch문으로 잡으려 하거나 throws로 던지지 않아도 되지만, 명시적으로 잡거나 throws 해줘도 된다.
첫번째 예외처리 방법은 예외 상황을 파악하고 문제를 해결해 정상화하는 복구다.
SQLException
이 발생하는 경우 재시도를 해볼 수 있다.```java
// 재시도를 통해 예외를 복구하는 예제 코드
int maxretry = MAX_RETRY;
while (maxretry -- > 0) {
try {
... // 예외 발생 가능성이 있는 시도
return; // 작업 성공
}
catch (someException e) {
// 로그 출력. 정해진 시간만큼 대기
}
finally {
// 리소스 반납. 정리 작업.
}
}
```
즉 예외의 처리란 사용자가 예외가 발생했다는 것을 알려야 할지라도,
애플리케이션 상에서는 정상적으로 설계된 작업 흐름을 따라 진행되어야 함을 의미한다.
예외처리를 강제하는 체크 예외들은 이렇게 예외를 어떻게든 복구할 가능성이 있는 경우에 사용한다.
API를 사용하는 개발자로 하여금 예외상황이 발생할 수 있음을 인식하도록 도와주고,
이에 대한 적절한 처리를 시도해보도록 요구하는 것이다.
예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다.
throws 문으로 선언해서 예외가 발생하면
것이다.
즉, 직접 처리하는 것을 회피하는 것이다.
예외처리를 회피하려면 반드시 다른 객체나 메소드가 예외를 대신 처리할 수 있도록 아래와 같이 던져줘야 한다.
public void add() throws SQLException {
// JDBC API
}
public void add() throws SQLException {
try {
// JDBC API
} catch(SQLException e) {
// 로그 출력
throw e;
}
}
JDBC와 같은 템플릿이 사용하는 콜백 객체들의 메소드에는 모두 throws SQLException
이 붙는다.
SQLException을 처리하는 일은 콜백 객체의 역할이 아니라고 보기 때문이다.
하지만 콜백과 템플릿처럼 긴밀하게 역할을 분담하고 있는 관계가 아니라면
자신의 코드에서 발생하는 예외를 그냥 던져버리는 건 무책임한 책임회피일 수 있다.
예외를 회피하려면 회피하는 의도가 분명해야 한다.
콜백/템플릿처럼 긴밀한 관계에 있는 다른 객체에게 예외처리 책임을 분명히 지게 하거나,
자신을 사용하는 쪽에서 예외를 다루는 게 최선의 방법이라는 확신이 있어야 한다.
예외를 복구해 정상 상태로 만들 수 없기에 예외를 메소드 밖으로 던지는 것이다.
예외 회피와 비슷하지만, 발생한 예외를 그대로 넘기지 않고 적절한 예외로 전환해서 던진다는 특징이 있다.
예외 전환은 보통 두 가지 목적으로 사용한다.
첫번째 목적은 예상하고 복구 가능한 예외 상황에서 사용한다.
(ex. 아이디 중복으로 인해 발생하는 SQLException)
public void add(User user) throws DuplicatieUserException, SQLException {
try {
// JDBC를 이용해 user 정보를 추가하는 코드,
// 또는 SQLException이 발생할 가능성이 있는 또 다른 메소드 코드
} catch (SQLExcpeption e) {
// Errorcode가 MySQL의 "Duplicate Enrty(1062)" 이면 예외 전환
if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) throw DuplicateUserIdException();
else throw e;
}
}
보통 전환하는 예외에 원래 발생한 예외를 담아 중첩 예외로 만드는 것이 좋다.
중첩 예외는 getCause() 메소드를 이용해 처음 발생한 예외에 관해 확인할 수 있다.
두번째 목적은 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸려는 경우에 사용한다.
대표적으로 EJBException
을 들 수 있다.
EJB 컴포넌트 코드에서 발생하는 대부분의 체크 예외는 비즈니스 로직으로 볼 때 의미있는 예외거나 복구 가능한 예외가 아니므로,
이런 경우 런타임 예외인 EJBException으로 포장해서 던지는 편이 낫다.
실제 엔터프라이즈 서버 환경에서는 수많은 사용자가 동시에 요청을 보내고,
각 요청이 독립적인 작업으로 취급된다.
애플리케이션 차원에서 예외상황을 미리 파악하고, 예외가 발생하지 않도록 차단하는 게 좋다.
또는 프로그램 오류나 외부 환경으로 인해 예외가 발생하는 경우,
빨리 해당 요청의 작업을 취소하고 서버 관리자나 개발자에게 통보해주는 편이 낫다.
자바의 환경이 서버로 이동하면서 체크 예외의 활용도와 가치는 점점 떨어지고 있다.
따라서 대응이 불가능한 체크 예외라면 빨리 RuntimeException
으로 전환해 던지는 것이 낫다.
최근의 표준 또는 오픈소스 프레임워크에서는 API가 발생시키는 예외를 체크 예외 대신 언체크 예외로 정의하는 것이 일반화되고 있다.
런타임 예외 중심의 전략은 복구할 수 있는 예외는 없다고 가정하고,
예외가 생겨도 어차피 런타임 예외이므로 시스템 레벨에서 알아서 처리해줄 것이고,
꼭 필요한 경우는 런타임 예외라도 잡아서 복구하거나 대응해줄 수 있으니 문제될 것이 없다는 낙관적인 태도를 기반으로 하고 있다.
반면 애플리케이션 외부의 예외상황이 원인이 아닌,
애플리케이션 자체 로직에 의해 의도적으로 발생시키고 반드시 catch해서 조치를 취하도록 요구하는 예외도 있다.
이런 예외들을 일반적으로 애플리케이션 예외라고 한다.
DAO를 굳이 따로 만들어서 사용하는 이유는,
데이터 액세스 로직을 담은 코드를 다른 성격의 코드들로부터 분리해놓기 위함이다.
하지만 DAO의 사용 기술과 구현 코드는 전략 패턴과 DI를 통해 이를 사용하는 클라이언트에게 감출 수 있지만,
메소드 선언에 나타나는 예외정보가 문제가 될 수 있다.
따라서 DAO의 인터페이스를 분리해 기술에 독립적인 인터페이스로 만들어야 한다.
그러기 위해선 인터페이스 도입과 예외 전환, 기술에 독립적인 추상화된 예외로 전환해야 한다.
DataAccessException
스프링은 자바의 다양한 데이터 액세스 기술을 사용할 때 발생하는 예외들을 추상화해서 DataAccessException
계층구조 안에 정리해놓았다.
🗣 스프링에서 제공하는 데이터 접근 계층의 모든 예외는 런타임 예외이다.
NonTransient 예외
: 일시적인 예외로, 동일한 SQL을 수행할 경우 성공할 가능성이 있다.
Transient 예외
: 일시적이지 않은 예외, SQL 문법 오류나 제약조건 위배 등
ObjectOptimisticLockinFalureException
으로 통일시켜 처리할 수 있다.IncorrectResultSizeDataAccessException
이 서브클래스로 정의되어 있다.DataAccessException이 기술에 구애받지 않고 어느정도 추상화된 공통 예외로 변환해주긴 하지만,
근본적인 한계 때문에 완벽함을 기대할 수는 없다.
만약 DAO에서 사용하는 기술의 종류와 상관없이 동일한 예외를 얻고 싶다면 직접 예외를 정의해두고 각 메소드에서 좀 더 상세한 예외 전환을 해줄 필요가 있다.