Clean Code 7장
7장 오류 처리
깨끗한 코드와 오류 처리는 확실히 연관성이 있다. 상당수 코드 기반은 전적으로 오류 처리 코드에 좌우된다.
오류 코드보다 예외를 사용하라
- 오류가 발생하면 예외를 던지는 편이낫다.
- 호출자 코드가 더 깔끔해진다.
- 논리가 오류 처리 코드와 뒤섞이지 않기 때문
- 각 개념을 독립적으로 살펴보고 이해할 수 있다.
- 호출자 코드가 더 깔끔해진다.
Try-Catch-Finally 문부터 작성하라
- try 블록은 트랜잭션과 비슷하다.
- try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.
- try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.
- 먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다.
- 자연스럽게 try 블록의 트랜잭션 범위부터 작성하게 되기 때문에 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.
unchecked 예외를 사용하라
- checked 예외는 OCP(Open Closed Principle)를 위반한다.
- 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 모두 고쳐야 한다.
- ex) 최상위 함수가 아래 함수를 호출하고 아래 함수는 그 아래 함수를 호출하는 시스템을 생각하자. 만약, 최하위 함수를 변경해 새로운 오류를 던진다고 가정한다면?
- 해당 함수를 호출하는 함수 모두가 변경이 일어난다.
- catch 블록에서 새로운 예외를 처리한다.
- 선언부에 throws 절을 추가한다.
- 즉, 연쇄적인 수정이 일어난다.
- throws 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하기 때문에 캡슐화가 깨진다.
- 해당 함수를 호출하는 함수 모두가 변경이 일어난다.
- checked 예외도 때로는 유용하다.
- 중요한 라이브러리를 작성한다면, 모든 예외를 잡아야 하기 때문이다.
예외에 의미를 제공하라
- 전후 상황을 충분히 덧붙인다.
- 오류가 발생한 원인과 위치를 찾기 쉬워진다.
- Java는 모든 예외에 호출 스택을 제공하지만, 실패한 코드의 의도를 파악하기에는 부족하다.
- 오류 메시지에 정보를 담아 예외와 함께 던진다.
- 실패한 연산 이름과 실패 유형도 언급한다.
- 로깅 기능을 사용한다면, catch 블록에서 오류를 기록하도록 충분한 정보를 넘긴다.
호출자를 고려해 예외 클래스를 정의하라
- 오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.
- 외부 API를 사용할 때는 감싸기 기법이 최선이다.
- 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.
- 나중에 다른 라이브러리로 갈아타도 비용이 적다.
- wrapper 클래스에서 외부 API를 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다.
- 특정 업체가 API를 설계한 방식에 발목 잡히지 않는다.
- 예외 클래스가 하나만 있어도 충분한 코드가 많다.
- 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우
- 한 예외는 잡아내고 다른 예외는 무시해도 괜찮은 경우는 여러 예외 클래스를 사용한다.
정상 흐름을 정의하라
- 비용 청구 애플리케이션에서 총계를 계산하는 코드를 보자.
try {
MealExpenses expenses = exponseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
m_total += getMealPerDiem();
}
- 식비를 비용으로 청구한 경우, 청구한 식비를 총계에 더한다.
- 식비를 비용으로 청구하지 않은 경우, 일일 기본 식비를 총계에 더한다.
=> 예외가 논리를 따라가기 어렵게 만든다.
MealExpenses expenses = exponseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
public class PerDiemMealExpenses implements MealExpenses {
public int getTotal() {
// 기본값으로 일일 기본 식비를 반환한다.
}
}
exponseReportDAO
를 고쳐 언제나 MealExpense
를 반환하도록 수정한다. 청구한 식비가 없다면, 일일 기본 식비를 반환하는 PerDiemMealExpenses
객체를 반환한다.
- 특수 사례 패턴(SPECIAL CASE PATTERN)
- 클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식
- 클라이언트 코드가 예외적인 상황을 처리할 필요가 없어진다.
null을 반환하지 마라
- null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다.
- 메서드에서 null을 반환하기 보다는 예외를 던지거나 특수 사례 객체를 반환한다.
- 외부 API가 null을 반환한다면, wrapper 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하는 방식을 고려한다.
null을 전달하지 마라
- 누군가 인수로 null을 전달한다면?
- 새로운 예외 유형을 만들어 던지는 방법
- 해당 예외를 잡아내는 처리기가 필요하다.
- assert 문을 사용하는 방법
- 코드는 읽기 편하지만, 문제를 해결하지는 못한다.
- 새로운 예외 유형을 만들어 던지는 방법
- 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.
결론
- 깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다.
- 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려한다.
- 튼튼하고 깨끗한 코드를 작성할 수 있다.
- 독립적인 추론이 가능해진다.
- 코드 유지보수성도 크게 높아진다.