욱'S 노트

Clean Code - 오류처리 본문

Methdology/Clean Code

Clean Code - 오류처리

devsun 2015. 7. 17. 18:37

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


얼마 전까지만 해도 예외를 지원하지 않는 프로그래밍 언어가 많았다. 예외를 지원하지 않는 언어는 오류를 처리하고 보고하는 방법이 제한적이었다. 당시의 방법에서는 호출자의 코드가 복잡해진다. 함수를 호출한 즉시 오류를 확인해야 하기 때문이다. 불행히도 이 단계는 잊어버리기 쉽다.


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


try 블록에서 무슨일이 생기든지 catch 블록은 프로그램 상태를 일관성 있게 유지해야 한다. 그러므로 예외가 발생할 코드를 짤때는 try-catch-finally 문부터 시작하는 편이 낫다


Unchecked 예외를 사용하라


확인된 예외가 반드시 필요하지 않다는 사실이 분명해졌다. 다양한 언어(C#, C++, 파이썬, 루비)에서 checked exception을 지원하지 않는데 안정적인 소프트웨어를 구현하는데 무리가 없다.


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


throws 경로에 위치한 모든 함수가 최하위 함수에서 던지는 예외를 알아야 하므로 캡슐화도 깨진다. 오류를 원거리에서 처리하기 위해 예외를 사용한다는 사실을 감안한다면 이처럼 확인된 예외가 캡슐화를 깨버리는 현상은 참으로 유감스럽다.


예외에 의미를 제공하라.


예외를 던질 때는 전후 상황을 충분히 덧붙인다. 오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 유형도 언급한다.


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


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


  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 {

    ...

  }



위의 코드는 다음과 같이 간결해 질 수 있다.


  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);

      }

    }

    ...

  }


실제로 외부 API를 사용할 때는 Wrapper가 최선이다. 외부 API를 감싸면 외부라이브러리와 프로그램 사잉에 의존성이 크게 줄어든다. 나중에 다른 라이브러리로 갈아타도 비용이 적게든다. 또한 감싸기 클래스에서 외부 API를 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다.

정상 흐름을 정의하라

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

위의 코드의 경우 예외가 논리를 따라가기 어렵게 만든다. 아래와 같이 DAO에서 항상 MealExpenses 객체를 반환하도록 변경한다면 훨씬 코드가 깔끔해 질 것이다.

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

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

위와 같은 패턴을 SPECIAL CASE 패턴이라고 한다.

null을 반환하지 마라

null을 확인이 누락된 문제라 말하기 쉽다. 하지만 실상은 null 확인이 너무 많아 문제다. 메서드에서 null을 반환하고픈 유혹이 든다면 그 대신에 예외를 던지거나 특수 사례 객체를 반환하자.

null을 전달하지 마라

메서드에서 null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쁘다. 피호출자측에서 항상 null을 고려하거나 assertion을 고려해야 한다.

결론 

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다. 이 둘은 상충하는 목표가 아니다. 오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다.


'Methdology > Clean Code' 카테고리의 다른 글

Clean Code - 경계  (0) 2015.07.20
Clean Code - 객체와 자료구조  (0) 2015.07.15
Clean Code - 형식 맞추기  (0) 2015.07.10
Clean Code - 주석  (0) 2015.07.08
Clean Code - 함수  (0) 2015.07.03
Comments