메서드가 저수준 예외를 처리하지 않고 바깥으로 전파해 버리면 수행하려는 일과 관련 없어 보이는 예외가 튀어나올 수 있다.
이는 내부 구현 방식을 드러내어 윗 레벨 API를 오염시킨다.
다음 릴리스에서 구현 방식을 바꾸면 다른 예외가 튀어나와 기존 클라이언트 프로그램을 깨지게 할 수도 있다.
저수준 예외
저수준 예외는 주로 하위 레벨의 컴포넌트나 라이브러리에서 발생하는 예외를 의미한다.
이 예외들은 주로 프로그램의 내부 동작, 하위 레벨의 리소스 접근, 네트워크 통신 등과 관련이 있다.
프로그램의 외부 요인으로 발생하거나, 하위 레벨에서 발생한 문제로 인해 발생하는 경우가 많아 애플리케이션 개발자가 직접 제어하기 어려울 수 있다.
- 파일을 열다가 발생한 입출력 예외
- 네트워크 연결 중에 발생한 소켓 예외
프로그램의 상위 레벨에서는 이러한 저수준 예외를 직접 처리하기보다는 더 추상화된 예외로 변환하거나, 메시지를 더욱 의미 있는 형태로 변환하여 상위 레벨로 전달하는 것이 좋다.
이렇게 함으로써 상위 레벨에서는 구체적인 내부 동작이나 하위 레벨의 구현과 독립적으로 예외를 처리할 수 있다.
저수준 예외를 처리하지 않았을 때
1. 수행하려는 일과 관련 없는 예외
- 메서드가 특정 작업을 수행하려는데 그와 관련 없는 예외가 발생하여 메서드 외부로 전파될 경우, 그 예외가 메서드 호출하는 측에서 처리해야 하는 예외와 연관이 없을 수 있다.
- 이로 인해 예외가 불필요하게 발생한 것처럼 보이며, 코드의 가독성과 이해도를 떨어뜨릴 수 있다.
- 메서드 내부에서 발생한 저수준 예외를 외부로 노출하면 해당 메서드의 내부 구현 방식이 노출된다.
- 이는 저수준의 구현 세부사항을 사용하는 클라이언트 코드에게 불필요한 정보를 제공하게 된다.
파일을 읽어서 내용을 반환하는 기능을 수행하는 readContentFromFile 메서드
public class FileReaderService {
public String readContentFromFile(String filePath) throws IOException {
FileReader reader = new FileReader(filePath);
// 파일에서 데이터 읽어오는 작업
// ...
return content;
}
}
- 파일 입출력 중에 IOException과 같은 저수준 예외가 발생할 수 있다.
- 이 예외를 그대로 클라이언트 코드에 노출시키면 클라이언트는 메서드 내부에서 파일을 읽는 방식이나 파일 처리에 대한 세부사항을 알게된다.
- 라이브러리나 프레임워크의 공개 API를 사용하는 클라이언트 프로그램에는 해당 API의 사용법과 예외 처리 방법이 포함된다.
- 만약 메서드 내부에서 처리하지 않은 예외가 바깥으로 노출되면, 그 예외의 처리도 클라이언트 프로그램의 책임이 된다. 이로 인해 클라이언트 코드의 복잡도가 증가하고, API를 사용하기 어려워질 수 있다.
파일을 다운로드하는 기능을 제공하는 FileDownloader 클래스
public class FileDownloader {
public void downloadFile(String url, String filePath) throws IOException {
// 파일 다운로드 작업 수행
// ...
}
}
- 메서드 내부에서 발생하는 IOException과 같은 예외를 그대로 클라이언트 코드에 노출시키면, 클라이언트 프로그램에서는 파일 다운로드 시 발생할 수 있는 예외를 처리해야 한다.
FileDownloader의 downloadFile 메서드를 호출하면서 예외 처리하는 ClientApp 클래스
public class ClientApp {
public static void main(String[] args) {
FileDownloader downloader = new FileDownloader();
try {
downloader.downloadFile("http://example.com/file.txt", "file.txt");
} catch (IOException e) {
// 클라이언트 코드에서 파일 다운로드 예외 처리
System.err.println("Error while downloading file: " + e.getMessage());
}
}
}
- 클라이언트 코드는 이 예외 처리를 위해 IOException과 관련된 처리 방법을 알아야 한다.
- 만약 라이브러리에서 예외가 변경되거나 추가된다면 클라이언트 코드도 그에 따라 수정해야 한다.
- 이로 인해 클라이언트 코드가 예외 처리에 관한 내용을 더 많이 알고 있어야 하며, API의 변화에 더 취약해질 수 있다.
- 이런 상황에서는 클라이언트 코드의 복잡도가 높아지고, API 사용이 어려워질 수 있다.
- 따라서 API를 사용하는 쪽에서는 이러한 문제를 최소화하기 위해 예외 번역과 추상화된 예외 처리를 고려하는것이 좋다.
- 릴리스 간에 내부 구현 방식을 변경하면 새로운 예외가 발생할 수 있다.
- 이 경우 기존 클라이언트 프로그램이 해당 예외를 처리하지 못하거나 오작동할 수 있다.
- 이는 API의 변경으로 인해 기존 클라이언트 코드를 수정해야 할 수도 있는 상황을 만들 수 있다.
예외 번역
상위 계층에서는 저수준 예외를 잡아 자신의 추상화 수준에 맞는 예외로 바꿔 던져야 한다.
위 예제의 readContentFromFile 메서드를 예외 번역 해보자.
애플리케이션의 추상화된 예외로, 실제 파일 입출력 예외를 래핑하여 노출시키는 FileReadingException 메서드
public class FileReaderService {
public String readContentFromFile(String filePath) throws FileReadingException {
try {
FileReader reader = new FileReader(filePath);
// 파일에서 데이터 읽어오는 작업
// ...
return content;
} catch (IOException e) {
throw new FileReadingException("Failed to read content from file", e);
}
}
}
- 이렇게 하면 클라이언트 코드는 파일 읽기 동작에 대한 구체적인 내용을 알 필요 없이 추상화된 예외로 처리할 수 있다.
- 또한 내부 구현 방식은 클라이언트 코드에 노출되지 않으므로 추후 파일 처리 방식 변경 시 클라이언트 코드에 영향을 주지 않는다.
예외 연쇄
예외 연쇄(Exception Chaining)는 한 예외가 다른 예외를 원인(cause)으로 갖는 형태를 의미한다.
- 예외 연쇄를 사용하면 상위 레벨의 예외가 하위 레벨에서 발생한 예외를 원인으로 포함시킬 수 있다.
- 이를 통해 클라이언트 코드에서는 상위 예외의 원인 예외를 추적하고, 문제의 근본적인 원인을 파악하는 데 도움을 받을 수 있다.
예외 연쇄를 구현하려면 예외 클래스의 생성자 중 하나를 사용하여 원인 예외를 지정해야 한다.
대표적인 생성자로 Exception이 있는데, 이 생성자를 사용하여 원인 예외를 전달하면 새로운 예외 객체가 생성되며 원인 예외가 해당 예외의 원인으로 설정된다.
예외 연쇄를 사용한 CustomException 예외 클래스
public class CustomException extends Exception {
public CustomException(String message, Throwable cause) {
super(message, cause);
}
}
public class Example {
public void performAction() throws CustomException {
try {
// 어떤 작업 수행
} catch (IOException e) {
throw new CustomException("An error occurred during action.", e);
}
}
}
public class Main {
public static void main(String[] args) {
try {
Example example = new Example();
example.performAction();
} catch (CustomException e) {
System.out.println("Caught CustomException: " + e.getMessage());
System.out.println("Root cause: " + e.getCause());
}
}
}
- performAction 메서드에서는 작업을 수행하다가 IOException이 발생할 경우 CustomException을 던진다.
- 이때 IOException을 원인으로 하는 예외 연쇄가 생성된다.
- 클라이언트 코드에서 CustomException을 처리할 때, getCause() 메서드를 통해 원인 예외를 확인할 수 있다.
- 이를 통해 예외가 어디에서 발생했는지 추적하고 문제의 근본적인 원인을 파악할 수 있다.
무턱대고 예외를 전파하는 것보다 예외 번역이 우수한 방법이지만, 그렇다고 남용해서는 곤란하다.
- 가능하다면 저수준 메서드가 반드시 성공하도록 하여 아래 계층에서는 예외가 발생하지 않도록 하는 것이 최선이다.
- 상위 계층 메서드의 매개변수 값을 아래 계층 메서드로 건네기 전에 미리 검사하여 예외를 피할 수 있다.
- 아래 계층에서의 예외를 피할 수 없다면, 상위 계층에서 그 예외를 조용히 처리하여 문제를 API 호출자에까지 전파하지 말자.
결론
- 아래 계층의 예외를 예방하거나 스스로 처리할 수 없고, 그 예외를 상위 계층에 그대로 노출하기 곤란하다면 예외 번역을 사용하자.
- 이때 예외 연쇄를 이용하면 상위 계층에는 맥락에 어울리는 고수준 예외를 던지면서 근본 원인도 함께 알려주어 오류를 분석하기에 좋다.
'Java > Effective Java' 카테고리의 다른 글
[Effective Java] Item 75. 예외의 상세 메시지에 실패 관련 정보를 담으라 (0) | 2023.08.12 |
---|---|
[Effective Java] Item 74. 메서드가 던지는 모든 예외를 문서화하라 (0) | 2023.08.11 |
[Effective Java] Item 72. 표준 예외를 사용하라 (0) | 2023.08.06 |
[Effective Java] Item 71. 필요 없는 검사 예외 사용은 피하라 (0) | 2023.08.06 |
[Effective Java] Item 70. 복구할 수 있는 상황에는 검사 예외를, 프로그래밍 오류에는 런타임 예외를 사용하라 (0) | 2023.08.05 |