핵심 요약
- 아래 계층의 예외를 예방하거나 스스로 처리할 수 없고, 그 예외를 상위 게층에 그대로 노출하기 어렵다면 예외 번역을 사용하라.
- 이때 예외 연쇄를 이용하면 상위 계층에는 맥락에 어울리는 고수준 예외를 던지면서 근본 원인도 함께 알려주어 오류를 분석하기 좋다.
예외 처리와 내부 구현 세부사항의 노출 문제
- API나 애플리케이션의 상위 레벨에서 예외를 처리할 때, 그 예외가 내부 구현의 세부 사항에 대한 정보를 드러내는 것은 좋지 않다.
- 그 이유는 내부 구현의 세부 사항은 변경될 수 있고, 그 변경이 고수준의 API 사용자에게 영향을 미칠 수 있기 때문이다.
예시 1
- Repository에서 JPA 특정 예외(ex. EntityNotFoundException)가 발생하면, 이를 Service나 Controller layer까지 전파하면 API 사용자에게 JPA에 대한 구체적인 정보가 노출된다.
예외 번역
✍ 상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다.
try {
...
} catch (LowerLevelException e) {
throw new HigherLevelException(...);
}
예시 1
예시 1의 문제점을 예외 번역을 통해 해결해보자.
- JPA의 EntityNotFoundException 예외를 잡고, 이를 더 상위 수준의 예외(ex. ProductNotfoundException)으로 변환하여 던질 수 있다.
- 이렇게 하면 API 사용자는 구체적인 정보를 몰라도 되며, 내부 구현이 바뀌더라도 API 사용자에게는 영향이 미치지 않는다.
// ProductService
public Optional<Product> findProductById(Long id) {
return productRepository.findById(id);
}
public void businessLogic(Long id) {
findProductById(id).ifPresentOrElse(
product -> {
// 상품이 있는 경우의 로직 처리
},
() -> {
// 상품이 없는 경우의 로직 처리
throw new ProductNotFoundException("Product with id " + id + " not found");
}
);
}
import javax.persistence.EntityNotFoundException;
public class ProductNotFoundException extends EntityNotFoundException {
public ProductNotFoundException(String message) {
super(message);
}
}
예시 2 - AbstractSequentialList
cf. AbstractSequentialList는 순차적인 데이터 접근을 필요로 하는 List 구현체에 대한 골격 구현을 제공한다.
/**
이 리스트 안의 지정한 위치의 원소를 반환한다.
@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);
}
}
- get 메소드는 주어진 인덱스에 해당하는 원소를 반환하거나, 해당 인덱스에 원소가 없을 경우 IndexOutOfBoundsException이 발생한다.
- 저수준의 NoSuchElementException을 고수준의 IndexOutOfBoundsException으로 번역하였다.
예외 연쇄
: 고수준 예외가 원인 저수준 원인 예외를 포함할 수 있도록 하는 기능
✍ 예외를 번역할 때, 저수준 예외가 디버깅에 도움이 된다면 사용하는 게 좋다.
// 예외 연쇄
try {
...
} catch (LowerLevelException cause) {
// 저수준 예외를 고수준 예외에 실어 보낸다.
throw new HigherLevelException(cause);
}
// 예외 연쇄용 생성자
class HigherLevelException extends Exception {
HigherLevelException(Throwable cause) {
super(cause);
}
}
- getCause 메소드는 예외 연쇄 시 사용된 저수준 예외를 반환하는 메소드이다.
- 이를 통해 원인 예외를 직접 접근할 수 있으며, 스택 추적 정보가 통합되어 디버깅에 유용하다.
- 대부분의 표준 예외는 예외 연쇄용 생성자를 갖추고 있다.
- 그렇지 않은 예외라도 Throwable의 initCause 메소드를 사용하여 예외 연쇄를 구현할 수 있다.
예시 1
import javax.persistence.EntityNotFoundException;
public class ProductNotFoundException extends EntityNotFoundException {
public ProductNotFoundException(String message, Throwable cause) {
super(message, cause);
}
}
// ProductService
public Optional<Product> findProductById(Long id) {
return productRepository.findById(id);
}
public void businessLogic(Long id) {
findProductById(id).ifPresentOrElse(
product -> {
// 상품이 있는 경우의 로직 처리
},
() -> {
EntityNotFoundException cause = new EntityNotFoundException("Product not found in repository");
throw new ProductNotFoundException("Product with id " + id + " not found", cause);
}
);
}
주의 사항
- 저수준 예외를 그대로 노출하는 것보다는 낫지만, 예외 번역을 남용하면 코드가 복잡해지고 예외의 원인을 추적하기 어려워질 수 있다.
- 따라서 가능하다면 입력 값을 사전에 검증하거나, 조건을 충족시키는 로직을 작성하는 등 예외가 발생하지 않도록 노력하자.
- ex. Spring Validation @NotEmpty
- 그래도 막을 수 없다면 상위 게층에서 예외를 적절히 처리하여 API 호출자에게 전파하지 않도록 하자.
- 이 경우 발생한 예외는 로깅 기능을 활용하여 기록하면 좋다.
- ex. Spring boot @SLF4J, @Logback
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 75. 예외의 상세 메시지에 실패 관련 정보를 담으라. (0) | 2023.08.12 |
---|---|
[effective java] 아이템 74. 메서드가 던지는 모든 예외를 문서화하라. (0) | 2023.08.12 |
[effective java] 아이템 72. 표준 예외를 사용하라. (0) | 2023.07.31 |
[effective java] 아이템 71. 필요 없는 검사 예외 사용은 피하라. (0) | 2023.07.31 |
[effective java] 아이템 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라. (0) | 2023.07.31 |