오류 처리

오류 처리하는 코드 작성하기

책너두 5기 15일차

로버트 C. 마틴의 클린코드 p.132 ~ p.141

내용 정리

7. 오류 처리

Try-Catch-Finally 문부터 작성하라

예시 : 파일이 없으면 예외를 던지는지 알아보는 단위 테스트.

1
2
3
4
@Test(expeccted = StroageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
    sectionStore.retrieveSection("invalid - file");
}

위 단위 테스트에 맞춰 다음 코드를 구현

1
2
3
4
public List<RecordedGrip> retrieveSection(String sectionName) {
    // 실제로 구현할 때까지 비어 있는 더미를 반환한다.
    return new ArrayList<RecordedGrip>();
}

위 코드는 예외를 던지지 않으므로 테스트는 실패. 코드를 변경한다.

1
2
3
4
5
6
7
8
public List<RecordedGrip> retrieveSection(String sectionName) {
    try {
        FileInputStream stream = new FileInputStream(sectionName)
    } catch (Exception e) {
        throw new StroageException("retrieval error", e);
    }
    return new ArrayList<RecordedGrip>();
}

이제 예외를 던지니 테스트가 성공한다. catch 블록에서 예외 유형을 좁혀 실제로 FileInputStream생성자가 던지는 FileNotFoundException을 잡아낼 수 있다.

1
2
3
4
5
6
7
8
public List<RecordedGrip> retrieveSection(String sectionName) {
    try {
        FileInputStream stream = new FileInputStream(sectionName)
    } catch (FileNotFoundException e) {
        throw new StroageException("retrieval error", e);
    }
    return new ArrayList<RecordedGrip>();
}

이후 TDD를 사용해 나머지 논리를 추가할 수 있다. 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하는 코드를 작성하라. try 블록의 트랜잭션 범위부터 구현하므로 본질을 유지하기 쉬워지기 때문.

미확인(unchecked)예외를 사용하라.

확인된 예외는 장점도 있고 이전에는 좋다고 여겨졌으나 지금은 다르다. 아주 중요한 라이브러리를 작성할땐 아직 중요하지만, 일반적으로는 비용을 생각하면 안쓰는게 맞다. 대규모 시스템에서, 최하위 함수가 새로운 오류를 던진다면 최상위까지 모두 수정해야해서 캡슐화가 깨져버린다.

예외에 의미를 제공하라.

예외를 던질 때 메시지에 전후 상황을 충분히 덧붙여 오류가 발생한 원인과 위치를 찾기 수월하게 하라.

호출자를 고려해 예외 클래스를 정의하라.

오류를 분류하는 방법은 많다. 발생한 위치, 발생한 컴포넌트, 유형 등. 오류를 잡아내는 방법은 중요하다. 외부 API를 사용할땐 감싸는 클래스를 사용하라. 호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하면 된다.

정상 흐름을 정의하라

지금까지를 바탕으로 코드를 작성하면 점차 오류 감지가 프로그램 언저리로 밀려난다. 때로는 예외를 따로 두는 것이 적절하지 않을 때도 있다.

1
2
3
4
5
6
try {
    MealExpenses expenses = expensesReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
} catch(MealExpensesNotFound e) {
    m_total += getMealPerDiem();
}

직원이 청구한 식비가 없다면? 일일 기본 식비를 더한다. 특스 상황을 처리할 필요가 없도록 하여 코드를 간결하게 할 수 있다.

1
2
MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();

더 간결하게 하면

1
2
3
4
5
public class PerDiemMealExpenses implements MealExpenses {
    public int getTotal() {
        // 기본값으로 일일 기본 식비를 반환
    }
}

이러한 것을 특수 사례 패턴(SPECIAL CASE PATTERN)이라고 부른다.

null을 반환하지 마라.

null을 반환하는 코드는 일거리를 늘리고, 호출자에게 문제를 떠넘긴다. 모든 부분에서 null 확인을 해야하는 번거로움이 생긴다. null 확인이 누락되어 생긴 문제는 null 확인이 너무 많은게 진짜 문제다.

예시:

1
2
3
4
5
6
List<Employee> employees = getEmployees();
if (employees != null) {
    for(Employee e : employees) {
        totalPay += e.getPay();
    }
}

null 반환을 바꿔보자.

1
2
3
4
List<Employee> employees = getEmployees();
for(Employee e : employees) {
    totalPay += e.getPay();
}

null을 전달하지 마라

assert를 통해 null이 전달되는 것을 막자.

1
2
3
4
5
6
7
8
public class MetricsCalculator
{
    public double xProjection(Point p1, Point p2) {
        assert p1 != null : "p1 should not be null";
        assert p2 != null : "p2 should not be null";
        return (p2.x - p1.x) * 1.5;
    }
}

대다수 프로그래밍 언어는 null을 적절히 처리하는 방법이 없기 때문에 애초에 넘기지 못하도록 금지하는 것이 합리적이다.

결론

읽기 좋은 코드와 안정성이 높은 코드는 상충하는 목표가 아니다.

읽고 나서

사실 예외에 대한 코드를 작성을 거의 하지 않는다. 우런 컴파일 해보고 오류가 발생하면 고치기 바쁘다. 꼭 명심해야할 파트다.