책 리뷰/클린코드

7장 오류처리

Lahezy 2023. 10. 14.
728x90

우아하고 고상하게 오류를 처리하는 기법과 고려 사항 몇 가지를 소개한다.

오류 코드보다는 예외를 사용해라

오류가 발생하면 예외를 던지는 것이 낫다. 그러면 호출자 코드가 더 깔끔해진다. 논리가 오류 처리 코드와 뒤 섞이지 않으니까.

public class DeviceController {
    DeviceHandle handle = getHandle(DEV1);
    if (handle != DeviceHandle.INVALID) {
        retrieveDeviceRecord(handle);
        if (record.getStatus() != DEVICE_SUSPENDED) {
            closeDevice(handle);
        } else {
            logger.log("Device suspended. Unable to shut down");
        }
    } else {
        logger.log("Invalid handle");
    }

}

예외를 던지고 코드를 분리하여 코드 품질이 높아졌다

public class DeviceController {
    public void sendShutDown() {
        try {
            tryToShutDown();
        }
        catch (DeviceShutDownError e) {
            logger.log(e);
        }
    }

    private void tryToShutDown() {
        DeviceHandle handle = getHandle(DEV1);
        DeviceRecord record = retrieveDeviceRecord(handle);

        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
    }

    private DeviceHandle getHandle(DeviceId id) {
        ...
        throw new DeviceShutDownError("Invalid handle for: " + id.toString());
        ...
    }

}

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

어떤 면에서 Try는 트랜잭션과 비슷하다. try 블록에서 무슨 일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다.
이를 통해 try 문제서 어떤 일이 발생하든 호출자가 기대하는 상태를 정의하기가 쉬워진다.

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

과거에는 확인된 예외를 멋진 아이디어라 생각하였지만 지금은 안정적인 소프트웨어를 제작하는 요소로 확인된 예외가 반드시 필요하지는 않다는 사실일 분명해졌다.

확인된 오류를 사용하는 경우 오버헤드에 대해 생각해야 한다. 확인된 예외는 OCP를 위반한다.

-> 상위의 코드에서 예외를 수정하며 하위의 코드들도 모두 수정해야 한다. 또한 단계가 내려갈수록 호출하는 함수 수가 늘어난다.

이로 인해 최상위부터 하위까지의 연쇄적인 수정이 일어난다. (캡슐화가 깨진다)

  • OCP(Open-Closed Principle) : 소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하지만 변경에 대해서는 닫혀 있어야 한다는 개념이다.

예외에 의미를 제공해라

예외를 던질 때는 전후 상황을 충분히 덧붙인다.

오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급하라.

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

오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 돼야 한다.

호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환한다.

LocalPort port = new LocalPort(12); //wrapper 클래스(예외를 하나만 반환한다)
try {
    port.open();
} catch (PortDeviceFailure e) { //예외를 하나만 반환한다
    reportError(e);
    logger.log(e.getMessage(), e);
} finally {
  ...
}
public class LocalPort {
    private ACMEPort innerPort;

    public LocalPort(int portNumber) {
        innerPort = new ACMEPort(portNumber);
    }

    public void open() {
        try {
            innerPort.open();
        } catch (DeviceResponseException e) {
            throw new PortDeviceFailure(e); //하나의 예외를 반환한다.
        } catch (ATM1212UnlockedException e) {
            throw new PortDeviceFailure(e); //하나의 예외를 반환한다.
        } catch (GMXEError e) {
            throw new PortDeviceFailure(e); //하나의 예외를 반환한다.
        }
    }

외부 API를 사용할 때는 Wrapper 클래스 기법이 최선이다.

정상 흐름을 정의하라

중단이 적합하지 않은 경우 특수사례 패턴으로 클래스를 만들거나 객체를 조작해 특수사례를 처리한다.

try {
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
} catch (MealExpensesNotFound e) {
    m_total += getMealPerDiem();
}

아래방식은 위의 코드에서 클래스를 만들거나 객체를 조작해 특수사례를 처리하는 방식이다. 이를 특수 사례 패턴이라 부른다.

위에서는 식비를 비용으로 청구했다면 직원이 청구한 식비를 총계에 더한다. 식비를 청구하지 않았다면 일일 기본식비를 총계에 더한다.
아래와 같이 expenseReportDAO를 수정하여 언제나 MealExpense 객체를 반환하다. 청구한 식비가 없다면 일일 기본식비를 반환하는 MealExpense객체를 반환하도록 한다.

MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();
public class PerDiemMealExpenses implements MealExpenses {
    public int getTotal(){
        //기본값으로 일일 기본 식비를 반환한다.
    }
}

null을 반환하지 마라

null은 예상치 못한 오류의 원인이 될 수 있다. null은 던져야 하는 상황이면 예외를 던지거나 특수 사례 객체를 반환하다.
사용하려는 외부 API가 null을 반환하려 하면 Wrapper를 구현하여 예외를 던지거나 특수 사례 객체를 반환하라.

 

1. Optional 사용

public void printName(String name) { 
    if (name != null) { //null임을 직접확인해야 한다.
        System.out.println(name);
    }
}

// 좋은 예시
public void printName(Optional<String> name) { //optional을 활용하면 null이 아닌경우에만 동작하도록 할 수 있다.
    name.ifPresent(System.out::println);
}

2. 예외 처리 (null을 반환하지 않고 예외를 반환한다)

public int divide(int a, int b) {
    if (b == 0) {
        throw new IllegalArgumentException("0으로 나눌 수 없습니다."); //null을 반환하는 것이 아니라 예외를 반환하도록 한다.
    }
    return a / b;
}

3.Collections.emptyList(); 와 같은 특수 사례 객체를 반환한다.

null을 전달하지 마라

null을 전달하는 경우 NullPointerException과 같은 다양한 오류가 발생할 수 있다.
예를 들어 아래와 같은 경우 오류가 발생할 수 있다.

String text = null;
int length = text.length(); // NullPointerException 발생
assert text!=null; //null이 아님을 확인해야한다. 
lenght = text.length()

이를 null을 체크하여 확인 후 실행 시키는 방식으로 수정될 수 있지만 코드의 디버깅과 유지보수적 측면에서 추천되지 않는 방법이다.

결론

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다. 오류 처리를 프로그램 논리와 분리하면 독립적인 추론이 가능해지며 코드 유지보수성도 크게 높아진다.

728x90

'책 리뷰 > 클린코드' 카테고리의 다른 글

6장 객체와 자료 구조  (0) 2023.07.25
5장 : 형식 맞추기  (0) 2023.06.28
클린코드 4장 : 주석  (0) 2023.06.21
3장 함수  (0) 2023.06.05
Clean Code(1,2 장)  (0) 2023.05.04

댓글