[엘레강트 오브젝트] 4.2 체크 예외(checked exception)만 던지세요
이 내용은 엘레강트 오브젝트
를 읽으면서 정리한 내용을 포함하고 있습니다.
- 체크 예외
- 언체크 예외
- 꼭 필요한 경우가 아니라면 예외를 잡지 마세요
- 항상 예외를 체이닝하세요
- 단 한번만 복구하세요
- 관점-지향 프로그래밍을 사용하세요
- 하나의 예외 타입만으로도 충분합니다
- 정리
저자 주장
언체크 예외를 사용하는 것은 실수이며, 모든 예외는 체크 예외여야 합니다. 또한 다양한 예외 타입을 만드는 것도 좋지 않은 생각입니다.
체크 예외
메서드의 시그니처의 throws IOException
을 사용한 예시는 다음과 같다.
public byte[] content(File file) throws IOException {
byte[] byte = new byte[1000];
new FileInputStream(file).read(array);
return array;
}
- 이것은 무슨 일이 있어도 content()를 호출하는 쪽에서 IOException 예외를 잡아야 한다는 것을 의미한다.
메서드는 시그니처에 throws IOException
이라고 선언함으로써 문제를 처리할 책임을 호출하는 쪽으로 넘긴다.
public byte[] content(File file) {
try {
return content(file).length();
} catch (IOException ex) {
// 이 예외에 대해 어떤 처리를 해야 하며 바로 여기에서 예외를 해결하거나 더 상위 레벨로 전달해야 한다.
}
}
- 파일에 문제가 발생했을 때 어떻게 처리할 지에 대한 결정을 호출하는 쪽에서 담당하게 하는 것이다.
다음과 같이 사용할 수 있다.
public int length(File file) throws IOException {
return content(file).length();
}
- 문제를 더 높은 상위 레벨로 확대시킨다.
- 상위 레벨(여기에서는 실제 호출하는 곳)에서는 이 문제를 처리하기 위해서 무언가를 해야 한다.
정리
- IOException은 catch 구문을 이용해서 반드시 잡아야 하기 때문에 체크(checked) 예외에 속한다.
- 예외를 잡거나 상위로 전파하기 위해 throws IOException을 메서드 시그니처에 선언해야 한다.
- 안전하지 않은 메서드를 다룰때에는 이 메서드를 사용하기 위해서 다음 중 하나를 해야 한다.
- 안전하지 않다고 선언
- 예외를 잡아서 해결
언체크 예외
무시할 수 있으며 예외를 잡지 않아도 무방합니다. 언체크 예외를 던지면, 누군가 예외를 잡기 전까지는 자동으로 상위로 전파된다.
- 예를 들어, 파일이 존재하지 않을 경우 던지는
IllegalArgumentException
은 언체크 예외에 속한다.
public int length(File file) throws IOException {
if (!file.exists()) {
throw new IllegalArgumentException(
"File doesn't exist; I can't count its length."
);
}
return content(file).length();
}
- 언체크 예외의 경우 예외의 타입을 선언하지 않아도 무방한 반면에 체크 예외는 항상 예외의 타입을 공개해야 한다.
꼭 필요한 경우가 아니라면 예외를 잡지 마세요
메서드를 설계할 때 모든 예외를 잡아서 메서드를 안전하게 만들지, 아니면 상위로 문제를 전파할지를 명확하게 선택해야 한다.
꼭 필요한 경우를 제외하고는 예외는 가급적 잡지 않는다.
흐름 제어를 위한 예외 사용
public int length(File file) {
try {
return content(file).length();
} catch (IOException ex) {
return 0;
}
}
- length() 메서드는 더할 나위 없이 안전하다.
- 파일 시스템에 어떤 일이 발생하더라도 메서드는 종료되지 않는다.
- 이 코드는 안전하게 실패하기 방법의 전형적인 예라고 할 수 있다.
- 에러 상황을 무시한 채 0을 반환해 버립니다.
동일하게 수행하는 다른 코드는 다음과 같다.
public int length(File file) {
if (/* 파일 시스템에 문제가 있으면 */) {
return 0;
} else {
return content(file).length();
}
}
- 분기를 처리하기 위해 if 문을 사용하는 것은 적절하지만, 예외를 분기를 위한 도구가 아니다.
- 예외를 분기를 처리할 목적으로 설계되지 않았다.
- 오퍼레이션의 정상적인 흐름을 종료시키고 추가적인 조치를 필요로 하는 심각하고 복구 불가능한 상황을 나타내기 위해 설계되었다.
정리
- 예외를 잡아 상황을 구조하는 일은 매우 정당한 이유가 있을 경우에만 용인되는 매우 중요한 행동이다.
항상 예외를 체이닝하세요
public int length(File file) throws Exception {
try {
return content(file).length();
} catch (IOException ex) {
throw new Exception("길이를 계산할 수 없다.", ex);
}
}
- 예외 체이닝은 원래의 문제를 새로운 문제로 대체함으로써 문제가 발생했다는 사실을 무시하지 않는다.
- 대신 원래의 문제를 새로운 문제로 감싸서 함께 상위로 던진다.
핵심은 문제를 발생시켰던 낮은 수준의 근본 원인을 소프트웨어의 더 높은 수준으로 이동시킨다는 것이다.
아래는 잘못된 방식의 예이다.
public int length(File file) throws Exception {
try {
return content(file).length();
} catch (IOException ex) {
// 여기에서는 문제 'ex'를 무시하고, 새로운 메시지를 가지는 새로운 타입의 새로운 문제를 생성한다.
throw new Exception("계산할 수 없다.");
}
}
- 문제를 발생시킨 근본 원인에 관한 매우 가치있는 정보가 손실되기 때문에 매우 방법이다.
항상 예외를 체이닝하고 절대로 원래 예외를 무시하면 안된다. 예외 체이닝은 의미론적으로 문제와 관련된 문맥을 풍부하게 만들기 위해 필요하다.
- 첫 번째 예외는 열린 파일이 너무 많다고 이야기하고
- 두 번째 예외는 파일의 길이를 계산할 수 없다고 이야기하고
- 마지막 예외는 이미지 내용을 읽을 수 없다고 이야기하도록 예외를 체이닝하는 편이 더 좋다.
정리
- 항상 예외를 체이닝하고 절대로 원래 예외를 무시하지 마세요.
단 한번만 복구하세요
예외 후 복구는 흐름 제어를 위한 예외 사용으로 알려진 안티패턴의 또 다른 이름이다.
아래는 예외 후 복구 방식을 적용한 안티패턴의 하나의 예이다.
int age;
try {
age = Integer.parseInt(text);
} catch (NumberFormatException ex) {
// 여기에서 발생한 예외를 `복구`한다
age = -1;
}
무조건 예외를 잡아서는 안되다는 주장은 전적으로 옮지 않으며 딱 한번은 복구해야 한다.
- 모든 메서드가 예외를 던진 후 해당 예외를 잡아서는 안된다.
- 모든 예외를 애플리케이션의 가장 높은 곳까지 전파될 것이다.
다음은 터미널을 통해 실행되는 커맨트 라인 도구의 진입점 예시이다.
public class App {
public static void main(String... args) {
try {
System.out.println(new App().run());
} catch (Exception ex) {
System.err.println("죄송하지만 문제가 발생했습니다." + ex.getLocalizedMessage());
}
}
}
- 코드에서 알 수 있는 것처럼 catch 문 내부에서는 어떤 것도 다시 던지지 않는다.
- 그 자리에서 즉시 문제를 해결하고 있다.
- 여기에서 해결방법이란 사용자에게 문제를 보여주는 것이 전부이다.
main에서 예외를 잡지 않는다면 런타임 환경으로 예외가 전달되고 결국 Java 가상 머신이 예외를 잡게 된다. 이 경우 사용자에게는 사용자 친화적인 메시지가 전달되지 않는다.
정리
- 어떤 소프트웨어에서든 복구에 적합한 몇 개의 장소를 제외하고는 예외를 잡아서 다시 던지거나, 또는 절대로 예외를 잡지 말아야 한다.
관점-지향 프로그래밍을 사용하세요
HTTP 요청을 전송해서 웹 페이지를 다운로드하는 경우를 가정하겠다.
- 가끔씩 네트워크 연결이 실패할 가능성이 있다.
- 이럴 때마다 사용자에게 오류 메시지를 표시하고 애플리케이션을 재실행하라고 요청해야 한다면 그것은 안타까운 일이다.
public String content() throws IOException {
int attempt = 0;
while (true) {
try {
return http();
} catch (IOException ex) {
if (attempt >= 2) {
throw ex;
}
}
}
}
관점-지향 프로그래밍을 통해 해결할 수 있는 방법
- 단 한번의 메서드 호출을 재시도하기 위해 10줄의 코드를 작성해야 한다는 사실을 알 수 있다.
이 코드는 매우 장황하면서도 원시적이다. 제대로 구현하려면 코드의 길이는 더 길어질 것이다.
@RetryOnFilure(attempts = 3)
public String content() throws IOException {
return http();
}
- 실패 재시도 코드 블록을 관점이라고 부른다.
- 기술적으로 관점이란 제어를 위임받아 content() 를 언제, 어떻게 호출할지 결정하는 객체를 의미한다.
정리
- AOP의 장점을 통해 핵심 클래스로부터 덜 중요한 기술과 매커니즘을 분리해서 코드 중복을 제거할 수 있다.
하나의 예외 타입만으로도 충분합니다
- 예외를 다시 던질 것이기 때문에 잡은 예외의 실제 타입에 대해서는 신경 쓸 필요가 없다.
- 예외를 사용할 일이 없기 때문에 예외의 타입 정보는 필요하지 않는다.
- 예외가 상위로 전파되는 도중에는 예외를 잡을 일이 없다.
- 예외를 잡을 때 조차도, 오직 한 가지 목적을 위해서만 잡아야 한다.
정리
IOException
은 catch 구문을 이용해서 반드시 잡아야 하기 때문에 체크(checked) 예외에 속한다.- 예외를 잡거나 상위로 전파하기 위해
throws IOException
을 메서드 시그니처에 선언해야 한다. - 반드시 예외를 잡아야 하는 이유가 있거나 다른 선택의 여지가 없는 경우가 아니라면 예외를 잡아서는 안된다.
- 어떤 소프트웨어에서든 복구에 적합한 몇 개의 장소를 제외하고는 예외를 잡아서 다시 던지거나, 또는 절대로 예외를 잡지 말아야 한다.
- AOP의 장점을 통해 핵심 클래스로부터 덜 중요한 기술과 매커니즘을 분리해서 코드 중복을 제거할 수 있다.
- 하나의 예외 타입만으로 충분한 이유는 예외를 상위로 다시 던질것이기 때문에 타입 정보는 필요하지 않다.