[Programming] - CleanCode 07. 오류 처리


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


요즘 언어들은 모두 예외를 지원하기때문에 예외로 오류를 처리해라.

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


어떤 면에서 try 문은 트랜잭션과 비슷하다.

try-catch 구조로 범위를 정하고 그다음 TDD로 나머지를 추가하라.

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

3. 미확인 예외를 사용하라.


여기서 미확인 예외란?

  • 확인된 예외는 컴파일 단에서 반드시 처리해야 하는 오류이다.
  • 미확인 예외는 런타임 단계에서 명시적 처리를 강제하지 않는 오류이다.

C#, C++, Dart에는 미확인 예외밖에 없다.

확인된 예외는 OCP 를 위반하기 때문에 미확인 예외를 사용하라.

확인된 예외를 사용하면 throws를 함수별로 다 추가해줘야한다.

4. 예외에 의미를 제공하라


호출 스택만으로는 부족하다. 가능한 정보를 제공하여 충분한 정보를 제공하자.

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

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


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

ACMEPort port = new ACMEPort(12);

try {
	port.open();
} catch(DeviceResponseExcetipn e) {
	reportError(e);
	logger.log("Device Response Exception", e);
} catch(ATM1212UnlockedException e) {
	reportError(e);
	logger.log("ATM1212UnlockedException", e);
} catch(GMXError e) {
	reportError(e);
	logger.log("GMXError", e);
}

위 오류 처리는 중복이 심하다. 하지만 많은 오류처리가 비슷한 방식으로 동작한다. 1) 오류를 기록한다.

2) 프로그램을 계속 수행할지 결정한다.

위의 경우에는 호출하는 라이브러리 API를 감싸서 하나의 예외 유형을 반환하면 된다.

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

	public void open() {
		try {
			innerPort.open();
		} catch(DeviceResponseExcetipn e) {
			throw new PortDeviceFailure(e);			
		} catch(ATM1212UnlockedException e) {
			throw new PortDeviceFailure(e);
		} catch(GMXError e) {
			throw new PortDeviceFailure(e);
		}
	}
}

이렇게 감싸면 API에 의전성또한 떨어진다.

6. 정상 흐름을 정의하라


위 충고를 잘 따른다면 비즈니스 논리와 오류 처리가 잘 분리된 코드가 나온다.

대게는 멋진 방법이지만, 때로는 중단이 적합하지 않은 때도 있다.

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

위 코드에서 식비를 비용으로 청구한 내용이 있다면 전체 합을 구하는 코드이다.

식비를 청구하지 않았다면 기본 식비를 총계에 더한다.

그런데 예외 코드가 논리를 따라가기 어렵게 방해한다.

특수 상황을 처리할 필요가 없게 수정해보자.

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

위 처럼 간결하게 하는 방법은 ExpenseReportDAO를 고쳐 언제나 반환하게 하는 방식이다.

public class PerDiemMealExpenses implements MealExpenses {
	public int getTotal() {
		//기본값을 일일 기본 식비로 반환 하게 작성
	}
}

이를 특수 사례 패턴이라 부른다.

클래스나 객체 내부에서 특수 케이스를 처리하는 방식이다.

7. null을 반환하지 마라


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

메서드에서 null을 반환하고 싶은 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환한다.

사용하려는 외부 API가 null을 반환한다면 감싸기 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하는 방식을 고려한다.

List<Employee> employees = getEmployees();
if(employees != null) {
	...
}

위 코드가 굳이 null을 반환하게 만들 이유가 없다. 빈 배열을 반환하게 하자.

그러면 null 체크가 사라질 것 이다.

8. null을 전달하지 마라


위와 비슷한 내용으로 파라미터에 null을 전달하면 해당 메서드에서 null체크를 해줘야한다.

만약 널처리를 해줘야한다면 assert로 처리하자.

정리


예외에 대해서 한번 감싸서 예외처리를 단순화 시키는 부분이 인상 깊었고,

null을 반환하지 마라는 부분이 많이 찔렸다.

Dart는 null-Safety가 적용되어있긴하지만

null 대신 예외를 던지도록 습관을 들여야 겠다.