7장: 예외 처리

오류 코드보다 예외를 사용하라 (130p)

얼마 전까지도 예외를 지원하지 않는 프로그래밍 언어가 많았다. 예외를 지원하지 않는 언어는 오류를 처리하고 보고하는 방법이 제한적이다.

예외 없이 오류를 처리하는 방법

  • 오류 플래그를 설정하기

  • 호출자에게 오류 코드를 반환하기

오류 코드/플래그 방식

public class DeviceController {
  // ...
  public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
    if (handle != DeviceHandle.INVALID) {
      retrieveDeviceRecord(handle);
      if (record.getStatus() != DEVICE_SUPENDED) {
        pauseDevice(handle);
        clearDeviceWorkQueue(handle);
      } else {
        logger.log("Device suspended. Unable to shut down");
      }
    } else {
      logger.log("Invalid handle for: " + DEV1.toString());
    }
  }
}

위와 같은 방법을 사용하면 호출자 코드가 복잡해진다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다. 만약 오류가 발생할 때만 예외를 던지게끔 바꾼다면?

예외를 던지는 코드

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 문부터 작성하라 (132p)

예외가 발생할 코드를 짤 때는 try-catch-finally 문으로 시작하는 편이 낫다. 그러면 try 블록에서 무슨 일이 생기든지 호출자가 기대하는 상태를 정의하기 쉬워진다.

미확인(Unchecked) 예외를 사용하라 (133-134p)

자바가 출시된 당시 확인된(Checked) 예외는 멋진 아이디어로 여겨졌다. 그러나 지금은 안정적인 소프트웨어를 제작하는 요소로 확인된 예외가 반드시 필요하지는 않다는 사실이 분명해졌다.

확인된 예외는 OCP를 위반한다. 메서드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다. 즉, 하위 단계에서 코드를 변경하면 상위 단계의 메서드 선언부를 전부 고쳐야 한다.

예외에 의미를 제공하라 (135p)

예외를 던질 때는 전후 상황을 충분히 덧붙인다. 그러면 오류가 발생한 원인과 위치를 찾기가 쉬워진다.

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

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

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

외부 라이브러리의 예외를 모두 잡으려는 코드

ACMEPort port = new ACMEPort(12);

try {
  port.open();
} catch (DeviceResposeException 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 {
  // ...
}

위 코드는 중복이 심하지만, 예외에 대응하는 방식이 예외 유형과 무관하게 거의 동일하다. 그래서 코드를 간결하게 고치기가 아주 쉽다.

예외 유형을 하나만 반환하는 코드

LocalPort port = new LocalPort(12);

try {
  port.open();
} catch (PortDeviceFailure e) {
  reportError(e);
  logger.log(e.getMessage(), e);
} finally {
  // ...
}

호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하는 방식이다. 여기서 LocalPort는 단순 ACMEPort 클래스가 던지는 예외를 잡아 변환하는 감싸기 클래스일 뿐이다.

감싸기 기법의 장점

  • 외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다. 나중에 다른 라이브러리로 갈아타도 비용이 적다.

  • 감싸기 클래스에서 외부 API를 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기 쉬워진다.

  • 특정 업체가 API를 설계한 방식에 발목 잡히지 않고, 프로그램이 사용하기 편리한 API를 정의할 수 있다.

정상 흐름을 정의하라 (137p)

앞 절의 충고를 충실히 따른다면 비즈니스 로직과 오류 처리가 잘 분리된 코드가 나온다. 그러나, 그러다 보면 오류 감지가 프로그램 언저리로 밀려나는 경우가 많다.

비용 청구 어플리케이션

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

식비를 비용으로 청구하지 않았다면 예외가 발생하여 기본 식비를 더한다. 하지만 이런 경우 예외가 논리를 따라가기 어렵게 만든다.

public class PerDiemMealExpenses implements MealExpenses {
  public int getTotal() {
    // 기본값 반환
  }
}

ExpenseReportDAO를 고쳐 청구한 식비가 없다면 일일 기본 식비를 반환하는 객체를 반환한다. 이렇게 하면 클라이언트가 예외적인 상황을 처리할 필요가 없다.

이를 특수 사례 패턴이라 한다!

null을 반환하지 마라 (138p)

우리가 흔히 오류를 유발하는 행위 중 하나가 null을 반환하는 습관이다. null을 반환하고 이를 확인하는 코드는 일거리를 늘릴 뿐만 하니라 호출자에게 문제를 떠넘긴다.

null을 반환하는 대신 예외를 던지거나 특수 사례 객체를 반환한다. 만약 외부 API가 그렇다면 감싸기 메서드를 구현하는 것이 낫다.

null을 전달하지 마라 (140p)

메서드로 null을 전달하는 방식은 더 나쁘다! 정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.

Last updated