예외 블랙홀
try{...}
catch(SQLException e){} // -> 예외를 잡고 아무것도 하지 않는다.
// 즉, 예외 발생을 무시해버리고 정상적인 상황처럼 넘어간다.
예외가 발생하면 그것을 catch 블록을 써서 잡아내는 것까진 좋은데 그리고 아무것도 하지 않고 별문제 없는 것처럼 넘어가 버리는 건 정말 위험한 일이다.
→ 예외가 발생했지만 그냥 무시해버리므로 예외로 인해 다른 기능이 비정상적으로 동작하면 예상치 못한 문제가 발생할 수 있다.
예외를 처리할 때 반드시 지켜야 할 핵심 원칙은 모든 예외는 적절하게 복구되든지 작업을 중단시키고 운영자나 개발자에게 분명하게 통보되어야 한다.
→ 예외를 무시하거나 잡아먹어 버리는 코드는 만들지 말아야한다.
무의미하고 무책임한 throws
public void method1() throws Exception {
...
}
위와 같이 메소드에 throws Exception
을 붙히는 경우가 있는데, 별 의미없이 기계적으로 붙히는 경우는 피해야 한다.
이렇게 특정한 에러가 아닌 그냥 모든 에러(Exception
)를 던지게 해놓으면 메소드 선언에서는 의미 있는 정보를 얻을 수 없어진다. 습관적으로 붙힌 것 인지 실행중 어떠한 에러가 발생할 수 있는 것인지를 알 수가 없다.
→ 결과적으로 적절한 처리를 통해 복구될 수 있는 예외상황도 제대로 다룰 수 있는 기회가 사라진다.
Error
java.lang.Error
클래스의 서브클래스들이다. 에러는 시스템에 비정상적인 상황이 발생했을 경우에 사용된다. 주로 자바 VM에서 발생시키는 것이고 애플리케이션 코드에서 잡으려고 하면 안된다.
→ 애플리케이션에서는 이러한 에러처리는 신경 쓰지 않아도 된다.
Exception과 체크 예외
java.lang.Exception
클래스와 그 서브클래스로 정의되는 예외들은 Error와 달리 개발자들이 만든 애플리케이션 코드의 작업 중에 예외상황이 발생했을 경우에 사용된다.
Exception 클래스는 체크 예외와 언체크 예외로 구분된다.
체크 예외는 위와 같이 Exception 클래스의 서브클래스이면서 RuntimeException 클래스를 상속하지 않은 것들이고, 언체크 예외는 RuntimeException을 상속한 클래스들을 말한다.
일반적으로 예외라고 하면 체크 예외라고 생각해도 된다. 체크 예외가 발생할 수 있는 메소드를 사용할 경우 반드시 예외를 처리하는 코드를 함께 작성해야 한다. 그렇지 않으면 컴파일 에러가 발생한다.
RuntimeException과 언체크/런타임 예외
java.lang.RuntimeException
클래스를 상속한 예외들은 명시적인 예외처리를 강제하지 않기 때문에 언체크 예외 혹은 런타임 예외라고 불린다.
런타임 예외는 주로 프로그램의 오류가 있을 때 발생하도록 의도된 것들이다. 예를 들면 NullPointerException
(할당되지 않은 레퍼런스 변수 사용)이나, IllegalArgumentException
(허용되지 않는 값을 사용하여 메소드 호출)등이 있다.
런타임 예외는 예시와 같이 코드에서 조건을 체크하도록 주의 깊게 만든다면 피할 수 있고, 예상하지 못했던 예외상황에서 발생하는 에러가 아니므로 처리를 강제하지 않아도 되게 만든 것이다.
예외 복구
예외 복구란 예외상황을 파악하고 문제를 해결해서 정상 상태로 돌려놓는 것을 말한다.
예를 들면 사용자가 요청한 파일을 읽으려고 시도했는데 해당 파일이 없다거나 다른 문제가 있어서 읽히지가 않아서 IOException이 발생했다고 생각해보자. 이때는 사용자에게 상황을 알려주고 다른 파일을 이용하도록 안내해서 예외상황을 해결할 수 있다.
즉, 예외로 인해 기본 작업 흐름이 불가능하면 다른 작업 흐름으로 자연스럽게 유도해주는 것이다.
또 다른 예시를 하나 들면 네트워크가 불안한 환경의 시스템이라면 DB서버에 접속하다 실패해서 SQLException이 발생하는 경우에는 재시도를 해볼 수 있다.
int maxretry = MAX_RETRY;
while(maxretry -- > 0) {
try {
... // 예외가 발생활 가능성이 있는 시도(ex: DB서버 접속)
return; // 작업 성공
catch(SomeException e) {
// 로그 출력. 정해진 시간만큼 대기
}
finally {
//리소스반납. 정리작업
}
}
throw new RetryFailedException(); // 최대 재시도 횟수를 넘기면 직접 예외 발생
위와 같이 최대 시도 횟수를 정해놓고, 시도가 실패할 때마다 정해진 시간만큼 대기한다. 이를 반복해서 작업이 성공하면 함수가 종료되고, 최대 시도 횟수가 넘어가면 Error를 발생시키는 식으로 예외를 복구할 수 있다.
예외처리 회피
예외처리 회피란 예외처리를 자신이 담당하지 않고 자신을 호출한 쪽으로 던져버리는 것이다.
throws 문으로 선언해서 예외가 발생하면 알아서 던져지게 하거나 catch 문으로 예외를 잡은 뒤 로그를 남기고 다시 던지는 식이다.
// 예외처리 회피 1
public void add() throws SQLException {
// JDBC API(SQLException 발생 가능)
}
// 예외처리 회피 2
public void add() throws SQLException {
try {
// JDBC API(SQLException 발생 가능)
}
catch(SQLException e) {
// 로그 출력
throw e;
}
}
이런 경우는 예외가 발생할 수 있는 메소드에서 예외를 처리하는 것이 아닌 메소드를 호출한 쪽에 예외를 넘겨서 호출한 쪽에서 예외를 처리하도록 하는 것이다.
이렇게 예외를 회피하는 경우는 예외를 받는 다른 오브젝트가 예외를 받았을 때 예외를 처리할 수 있어야 한다.
예외 전환
예외 전환은 예외 회피와 비슷하게 예외를 메소드 밖으로 던진다. 대신, 예외를 적절한 예외로 전환해서 던진다는 점이 다르다.
예외 전환은 내부에서 발생한 예외를 그대로 던지는 것이 그 예외상황에 대한 적절한 의미를 부여해주지 못하는 경우에 의미를 분명하게 해줄 수 있는 예외로 바꿔주기 위해서 사용한다.
예를 들면 새로운 사용자를 등록하려고 시도했을 때 아이디가 같은 사용자가 있어서 DB에러가 발생하면 SQLException
이 발생한다.
이 경우 SQLException
을 그대로 던져버리면 예외가 발생한 이유를 쉽게 알 수 없다.
이럴 땐 SQLException
의 정보를 해석해서 DuplicateUserIdException
같은 예외로 바꿔서 던져주는 것이 좋다.
아래 코드는 사용자 정보 등록을 시도하고 만약 중복된 아이디 값 때문에 에러가 나는 경우 DuplicateKeyException
으로 전환해주는 DAO 메소드의 예시이다.
public void add(User user) throws DuplicateUserldException, SQLException {
try {
// JDBC를 이용해 user 정보를 DB에 추가하는 코드 또는
// 그런 기능을 가진 다른 SQLException을 던지는 메소드를 호출하는 코드
}
catch(SQLException e) {
// ErrorCode가 MySQL의 "Duplicate Entry(1062)“01면 예외 전환
if (e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY)
// 중첩 예외로 만들어서 처음 발생한 예외를 SQLException로 설정
throw DuplicateUserldException().initCause(e);
else
throw e; // 그 외의 경우는 SQLException 그대로
}
}
보통 전환하는 예외에 원래 발생한 예외를 담아서 중첩 예외로 만드는 것이 좋다. initCause
메소드(혹은 생성자)로 위와 같이 원인이 되는 예외를 넣어주면 된다.
이와 같이 의미가 분명한 예외가 던져지면 서비스 계층에서는 적절한 복구 작업을 시도할 수 있다.
또 다른 전환 방법은 예외를 처리하기 쉽고 단순하게 만들기 위해 포장하는 것이다.
주로 예외처리를 강제하는 체크 예외를 언체크 예외인 런타임 예외로 바꾸는 경우에 사용한다.
예를 들면 체크 예외중에 비즈니스 로직으로 볼 때 의미있는 예외가 아니거나 복구 가능한 예외가 아닌 경우에는 체크 예외를 런타임 예외로 포장하여 던지는 편이 낫다.
반대로 애플리케이션 로직상에서 예외상황이 발생할 수도 있다. 이런 것은 애플리케이션 코드에서 의도적으로 던지는 예외이고, 체크 예외를 사용하는 것이 적절하다.
하지만 체크 예외를 계속 throws를 사용해서 넘기는 것은 무의미하다. 복구가 불가능한 예외라면 가능한 한 빨리 런타임 예외로 포장해서 다른 계층의 메소드에서 불필요한 throws 가 들어가지 않도록 해야한다.
이처럼 복구하지 못할 예외라면 런타임 예외로 포장해서 던져버리고, 관리자와 사용자에게 알리는 식으로 처리하는 것이 좋다.