먼저 Error와 Exception이 무엇인지 알 필요가 있다.
Error는 시스템에 비정상적인 상황이 생겼을 때 발생한다. 이는 주로 자바 가상머신에서 발생시키기 때문에 애플리케이션에서 오류에 대한 처리를 신경 쓰지 않아도 된다. Error의 예로는 OutOfMemoryError, ThreadDeath, StackOverflowError등이 있다.
Exception는 개발자가 구현한 로직에서 발생한다. 예를 들어 입력 값에 대한 처리가 불가능하거나, 프로그램 실행 중에 참조된 값이 잘못된 경우 등 정상적인 프로그램의 흐름을 어긋나는 경우이다. 개발자가 처리할 수 있기 때문에 Exception을 구분하고 그에 따른 처리방법을 명확히 알고 적용하는 것이 중요하다.
위 그림은 Exception class의 구조이다. 모든 Exception class는 Throwable class를 상속받고 있으며, Throwable은 최상위 class인 Object의 하위 class이다.
Checked Exception
Unchecked Exception
그렇다면 Unchecked Exception은 예외 처리를 강제하지 않는 이유는 무엇일까?
public class ArrayTest {
public static void main(String[] args) {
try {
int[] list = {1, 2, 3, 4, 5};
System.out.println(list[0]);
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
}
}
}
예외 처리를 강제할 경우 위와 같이 배열을 만들어 배열의 원소를 출력하고자 할 때 try/catch문을 매번 사용해주어야 한다. 이러한 RuntimeException은 개발자들에 의해 실수로 발생하는 것들이 대부분이기 때문에 에러를 강제하지 않는 것이다.
Oracle Java Documentation에서 다음과 같은 가이드를 제공한다.
Exception으로부터 복구할 수 있으면 Checked Exception으로 아무것도 수행할 수 없으면 Unchecked Exception으로 만들어라
예를 들어 파일을 열 때 파일 이름을 검증해야한다. 만약 유저의 input 파일이 유효하지 않은 이름을 가진다면 다음과 같이 Checked Exception을 던질 수 있다.
if (!isCorrectFileName(fileName)) {
throw new IncorrectFileNameException("Incorrect filename : " + fileName );
}
이런 방식으로 다른 유저의 input 파일을 받으면서 시스템을 복구할 수 있다.
하지만 input 파일 이름이 null pointer거나 비어있는 문자열이라면 코드에 문제가 있는것이다. 이런 경우에는 다음과 같이 Unchecked Exception을 던진다.
if (fileName == null || fileName.isEmpty()) {
throw new NullOrEmptyException("The filename is null or empty.");
}
임의의 Exception class를 만들어 예외 처리를 할 때 Checked Exception class를 만들고 싶으면 Exception class를 확장하고 Unchecked Exception class를 만들고 싶으면 RuntimeException class를 확장하면 된다.
1) 예외 복구
final int MAX_RETRY = 100;
public Object someMethod() {
int maxRetry = MAX_RETRY;
while(maxRetry > 0) {
try {
...
} catch(SomeException e) {
// 로그 출력. 정해진 시간만큼 대기한다.
} finally {
// 리소스 반납 및 정리 작업
}
}
// 최대 재시도 횟수를 넘기면 직접 예외를 발생시킨다.
throw new RetryFailedException();
}
2) 예외 처리 회피
// 예시 1
public void add() throws SQLException {
// ...생략
}
// 예시 2
public void add() throws SQLException {
try {
// ... 생략
} catch(SQLException e) {
// 로그를 출력하고 다시 날린다!
throw e;
}
}
3) 예외 전환
// 조금 더 명확한 예외로 던진다.
public void add(User user) throws DuplicateUserIdException, SQLException {
try {
// ...생략
} catch(SQLException e) {
if(e.getErrorCode() == MysqlErrorNumbers.ER_DUP_ENTRY) {
throw DuplicateUserIdException();
}
else throw e;
}
}
// 예외를 단순하게 포장한다.
public void someMethod() {
try {
// ...생략
}
catch(NamingException ne) {
throw new EJBException(ne);
}
catch(SQLException se) {
throw new EJBException(se);
}
catch(RemoteException re) {
throw new EJBException(re);
}
}