메서드가 저수준 예외를 처리하지 않고 바깥으로 전파( throws
)해버릴 때, 수행하려는 일과 관련 없어 보이는 예외가 튀어나온다. 이는 내부 구현 방식을 드러내어 윗 레벨의 API를 오염시킬 수 있으므로 위험하다.
대표적인 예시로, 스프링에서도 데이터 계층에서 특정 DB에 종속되는 SQLException
과 같은 저수준 예외를, 예외 변환기 ( PersistenceExceptionTranslator
) 를 통해 고수준 DataAccessException
으로 변환시킨다.
이 문제를 피하려면, 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꾸어 던져야 한다. 그리고 이를 예외 번역이라 한다.
try {
...
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
예외 번역을 사용한 예로 AbstractSequentialList
를 들어보자.
AbstractSequentialList
는 List
인터페이스의 골격 구현(아이템 20)이다. 이 예에서 수행한 예외 번역은, List<E>
인터페이스의 get
메서드 명세에 명시된 필수사항이다.
/**
이 리스트 안의 지정한 위치의 원소를 반환한다.
@throws IndexOutOfBoundsException index가 범위 밖이라면,
즉 {@code index < 0 || index >= size()}이면 발생한다.
**/
public E get(int idex) {
ListIterator<E> i = listIterator(index);
try {
return i.next();
} catch (NoSuchElementException e) {
throw new IndexOutOfBoundsException("인덱스: " + index);
}
}
예외 연쇄란, 문제의 근본 원인인 저수준 예외를 고수준 예외에 실어 보내는 방식이다. 따라서 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 예외 연쇄를 사용하도록 하자.
try {
...
} catch (LowerLevelException cause) {
// 저수준 예외를 고수준 예외에 실어 보낸다.
throw new HigherLevelException(cause);
}
대부분의 표준 예외는 아래와 같이 예외 연쇄용 생성자를 갖추고 있다.
class HigherLevelException extends Exception {
HigherLevelException(Throwable cause) {
super(cause);
}
}
문제의 원인을 getCause
메서드로 프로그램에서 접근할 수 있게 해주어, 원인과 고수준 예외의 스택 추적 정보를 잘 통합해준다.
결론적으로 예외를 전파( throws
)하는 것보다 예외 번역이 더 나은 방법이지만, 남용해서는 안된다. 우선순위는 다음과 같다.
만약 아래 계층에서의 예외가 불가피하다면, 상위 계층에서 예외를 조용이 처리하여 API 호출자에게까지 전파하지 않는 방법도 존재한다. 이 경우, 발생한 예외는 java.util.logging
같은 로깅 기능을 활용하여 기록해두자.
📚 핵심 정리
아래 계층의 예외를 예방하거나 스스로 처리할 수 없고, 상위 계층에 그대로 노출하기 곤란하다면 예외 번역을 사용하자. 이때 예외 연쇄를 이용하면 상위 계층에는 맥락에 어울리는 고수준 예외를 던지면서, 근본 원인도 함께 알려주어 오류를 분석하기에 좋다.