Language/Java

[클린 코드] Ch.7 - 오류 처리

비소_ 2022. 8. 12.

클릭하시면 네이버 Book으로 연결됩니다!

해당 서적을 참고하여 개인 공부용으로 정리한 글입니다.


오류 코드보다 예외를 사용할 것

Ch.3 함수에서 얘기했듯이, 오류 코드보다 예외를 던져 처리하는 것이 낫다.

그래야 논리와 오류 처리가 섞여 프로그램을 이해하기 어려워지는 일이 없어진다.


Try-Catch-Finally 문부터 작성할 것

try 블록에서 무슨 일이 생기든 catch 블록은 프로그램 상태를 일관성있게 유지해야 한다.

따라서 예외가 발생할 코드는 try-catch-finally 문으로 시작하는 편이 낫다.

강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다.

그러면 자연스럽게 try 블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.


미확인 예외를 사용할 것

확인된(checked) 예외는 OCP(Open Closed Principal)를 위반한다.

대규모 시스템에서 최상위 함수는 최하위 함수까지 연쇄적으로 호출한다.

만약, 최하위 함수가 새로운 확인된 오류를 던진다면 함수는 선언부에 throws 절을 추가해야 한다.

이후, 이 함수를 호출하는 모든 함수가 catch 블록에 새로운 예외를 처리하거나, 선언부에 thorws를 추가해야 한다.

모든 함수가 최하위 함수애서 던지는 예외를 알아야 하므로 캡슐화가 깨진다.


예외에 의미를 제공할 것

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

그러면 오류가 발생한 원인과 위치를 찾기가 쉬워진다.

실패한 코드의 의도를 파악하려면 호출 스택만으로 부족하다.

실패한 연산의 이름과 실패 유형 등 정보를 담는다.

로깅 기능을 사용한다면, catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.


호출자를 고려해 예외 클래스를 정의할 것

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

아래 코드는 오류를 형편없이 분류한 사례다.

  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 {
    ...
  }

오류를 처리할 때 대부분 오류를 기록하고, 계속 수행해도 좋은지 확인한다.

따라서, 위처럼 중복되는 내용이 많아진다.

호출하는 라이브러리 API를 감싸면서 예외 유형 하나를 반환하면 쉽게 해결할 수 있다.

  LocalPort port = new LocalPort(12);
  try {
    port.open();
  } catch (PortDeviceFailure e) {
    reportError(e);
    logger.log(e.getMessage(), e);
  } finally {
    ...
  }
  
  //ACMEPort 클래스가 던지는 예외를 잡아 변환하는 Wrapper 클래스
  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를 감싸면 외부 라이브러리와 프로그램의 의존성이 크게 줄어든다.


정상 흐름을 정의할 것

위 규칙대로 코드를 작성하면 깨끗하고 간결한 코드가 탄생한다.

하지만 오류 감지가 프로그램 언저리로 밀려난다.

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

이런 상황의 경우 더 지저분한 코드가 탄생한다.

ExpenseReportDAO를 고쳐 언제나 MealExpenses 객체를 반환하게 만들면 catch 블록이 없어져도 된다.

  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 pattern)이라 부른다.

클래스를 만들거나 객체를 조작해 특수 사례를 처리하는 방식이다.


null을 반환하지 말 것

null 확인 코드는 너무 지저분하다.

null을 절대 반환하지 않는다는 보장이 있다면 더 효율적으로 작성할 수 있을 것이다.

null을 반환하고싶은 생각이 들면 예외를 던지거나 특수 사례 객체를 반환한다.

많은 경우 특수 사례 객체가 손쉬운 해결책이다.


null을 전달하지 말 것

메서드로 null을 전달하는 방식은 더 나쁘다.

정상적인 인수로 null을 기대하는 API가 아니라면 최대한 피한다.

대다수 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다.

따라서 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.


깨끗한 코드는 안정성도 높아야 한다.

오류 처리를 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다.

댓글