∥♥∥小중한。인연으로。남길바래요∥♡∥

[Clean Code] 7.오류 처리

2021年03月07日

클린코드 7장 오류 처리

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

옛날에는 예외를 지원하지 않는 프로그래밍 언어가 많았다. 그래서 오류 플래그를 설정하거나 호출자에게 오류 코드를 반환하는 방법을 사용했었다.

이와 같은 방법을 사용하면 호출자 코드가 복잡해지고 함수 호출과 동시에 오류를 확인해야한다.

오류가 발생하면 예외를 던져 논리와 오류 처리 코드를 분리하여 호출자 코드가 더 깔끔해지도록 한다.

// Bad
public class DeviceController {
  ...
  public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
    // Check the state of the device
    if (handle != DeviceHandle.INVALID) {
      // Save the device status to the record field
      retrieveDeviceRecord(handle);
      // If not suspended, shut down
      if (record.getStatus() != DEVICE_SUSPENDED) {
        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
        closeDevice(handle);
      } else {
        logger.log("Device suspended. Unable to shut down");
      }
    } else {
      logger.log("Invalid handle for: " + DEV1.toString());
    }
  }
  ...
}
// Good
public class DeviceController {
  ...
  public void sendShutDown() {
    try {
      tryToShutDown();
    } catch (DeviceShutDownError e) {
      logger.log(e);
    }
  }

  private void tryToShutDown() throws DeviceShutDownError {
    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 문부터 작성하라

옛날에는 메서드를 선언할 때 메서드가 반환하는 예외를 모두 열거했다. 하지만 지금은 확인된 예외를 잘 사용하지 않는다.

확인된 오류가 치르는 비용에 상응하는 이익을 제공하는지 따져봐야 한다.

확인된 예외는 OCP 를 위반한다.

메서드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메서드 모두가 선언부에 해당 예외를 정의해 한다.

throws 경로에 위치하는 모든 함수가 최하위 함수에서 던지는 예외를 알아야하기 때문에 캡슐화가 깨진다.

아주 중요한 라이브러리의 경우 모든 예외를 잡아야하지만 일반적인 애플리케이션에서는 의존성이라는 비용이 이익보다 크다.

미확인 예외를 사용하라

오류 메시지에 정보를 담아 예외와 함께 던져라

예외에 의미를 제공하라

오류를 분류하는 방법은 수없이 많다.

  • 오류가 발생한 위치 - 컴포넌트로 분류 유형으로 분리
  • 유형으로 분리 - 디바이스 실패, 네트워크 실패. 프로그래밍 오류 등으로 분류

하지만 오류를 정의할 때 가장 중요한 것은 오류를 잡아내는 방법이다.

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

// Bad
  ACMEPort port = new ACMEPort(12);
  try {
    port.open();
  } catch (DeviceResponseException e) {
    reportPortError(e);
    logger.log("Device response exception", e);
  } catch (ATM1212UnlockedException e) {
    reportPortError(e);
    logger.log("Unlock exception", e);
  } catch (GMXError e) {
    reportPortError(e);
    logger.log("Device response exception");
  } finally {
    ...
  }
// Good
LocalPort port = new LocalPort(12);
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 (GMXError e) {
      throw new PortDeviceFailure(e);
    }
  }
  ...
}

정상 흐름을 정의하라

앞 절의 내용을 따르면 비지니스 논리와 오류 처리가 잘 분리된 코드가 나온다.

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

  public class PerDiemMealExpenses implements MealExpenses {
    public int getTotal() {
      // return the per diem default
    }
  }

  // 이해를 돕기 위해 직접 추가한 클래스
  public class ExpenseReportDAO {
    ...
    public MealExpenses getMeals(int employeeId) {
      MealExpenses expenses;
      try {
        expenses = expenseReportDAO.getMeals(employee.getID());
      } catch(MealExpensesNotFound e) {
        expenses = new PerDiemMealExpenses();
      }

      return expenses;
    }
    ...
  }

null을 반환하지 마라

null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다.

// Bad
  List<Employee> employees = getEmployees();
  if (employees != null) {
    for(Employee e : employees) {
      totalPay += e.getPay();
    }
  }
// Good
List<Employee> employees = getEmployees();
for(Employee e : employees) {
  totalPay += e.getPay();
}

public List<Employee> getEmployees() {
  if( .. there are no employees .. )
    return Collections.emptyList();
  }
}

null을 전달하지 마라

메서드에서 null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쁘다.

대다수의 프로그래밍 언어는 호출자가 실수로 넘가는 null을 적절히 처리하는 방법이 없어 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.

결론

깨끗한 코드는 읽기도 좋아야하지만 안정성도 높아야 한다. 오류 처리를 프로그램 논리와 분리하면 튼튼하고 깨끗한 코드를 작성할 수 있다.