[CleanCode] -7. 오류 처리

Young Min Sim ·2021년 4월 19일
0

CleanCode

목록 보기
7/16

오류처리는 중요하다.
하지만, 오류 처리 코드로 인해 프로그램 논리를 이해하기 어려워진다면 깨끗한 코드라 부르기 어렵다.
깨끗한 코드와 오류 처리는 연관성이 있다.


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

public class DeviceController {
  ...
  
  public void sendShutDown() {
    DeviceHandle handle = getHandle(DEV1);
    if (handle != DeviceHandle.INVALID) {
      retrieveDeviceRecord(handle);
      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());
    }
  }
  ...
}

호출한 함수가 적절한 결과값 뿐만 아니라 오류처리를 위한 결과값까지 반환을 하기 때문에 복잡해진다.
따라서 예외를 던지는 편이 낫다.

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

예외 처리도 한가지 작업.
한 가지 함수에서 한 가지 작업만 하는게 좋다.


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

예시) 파일을 열어 직렬화된 객체 몇 개를 읽어 들이는 코드가 필요.

다음은 파일이 없으면 예외를 던지는지 알아보는 단위 테스트다.

@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
  sectionStore.retrieveSection("invalid - file");
}

1단계

다음 코드는 예외를 던지지 않으므로 단위테스트는 실패한다.

public List<RecordedGrip> retrieveSection(String sectionName) {
  return new ArrayList<RecordedGrip>();
}

2단계

다음 코드는 예외를 던진다. 따라서 테스트는 성공한다.

public List<RecordedGrip> retrieveSection(String sectionName) {
  try {
    FileInputStream stream = new FileInputStream(sectionName);
  } catch (Exception e) {
    throw new StorageException("retrieval error", e);
  }
  return new ArrayList<RecordedGrip>();
}

3단계

catch 블록의 예외 유형을 좁혀 실제로 FileinputStream 생성자가 던지는 FileNotFoundException 을 잡아낸다.

public List<RecordedGrip> retrieveSection(String sectionName) {
  try {
    FileInputStream stream = new FileInputStream(sectionName);
    stream.close();
  } catch (FileNotFoundException e) {
    throw new StorageException("retrieval error", e);
  }
  return new ArrayList<RecordedGrip>();
}

'파일을 열어 직렬화된 객체 몇 개를 읽어 들이는 코드가 필요' 했으므로 TDD를 사용해,
FileInputStream 을 생성하는 코드와 close 호출문 사이에 나머지 논리를 채워 넣으면 됨.


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

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

아래 코드는 예외 유형과 관계 없이 예외에 대응하는 방식(catch 블럭)이 거의 동일하다.
그래서 코드를 간결하게 고치기 쉽다.
호출하는 라이브러리 API 를 감싸면서 예외 유형 하나를 반환하면 된다.

감싸기 전

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

// 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를 사용할 때는 감싸기 기법이 최선이다.

  • 외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.
  • 나중에 다른 라이브러리로 갈아타도 비용이 적다.
  • 감싸기 클래스에서 실제 외부 API를 호출하는 대신 테스트 코드를 넣어주는 방법으로 프로그램을 테스트하기도 쉬워진다.
  • 특정 업체가 API를 설계한 방식에 발목 잡히지 않는다. 사용하기 편리하게 정의하면 그만이다.
    ex) 예제에서 port 디바이스 실패를 표현하는 예외 유형 하나를 정의

4. 정상 흐름을 정의하라

때로는 try catch 를 이용하여 예외를 던지고, 중단된 계산을 처리하는게 적합하지 않은 때도 있다.

아래 코드는 예외가 논리를 따라가기 어렵게 만든다.
특수한 상황을 처리할 필요가 없다면 더 좋지 않을까?


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

// 식비를 비용으로 청구하지 않은 경우 반환되는 클래스
public class PerDiemMealExpenses implements MealExpenses {
  public int getTotal() {
    // 기본값으로 일일 기본 식비를 반환한다.
  }
}

5. null 을 반환하지 마라

null 값을 반환하는 것은 호출자에게 문제를 떠넘기는 나쁜 코드
null 을 반환하고픈 유혹이 든다면 그 대신,

  • 예외를 던지거나
  • 특수 사례 객체를 반환

null 값 반환하는 코드

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

null 값 반환하지 않는 코드

  • 예외 상황 처리를 호출자에게 떠넘기지 않음
  • 코드가 더 간결해짐
List<Employee> employees = getEmployees();
for(Employee e : employees) {
  totalPay += e.getPay();
}

public List<Employee> getEmployees() {
  if ( .. 직원이 없다면 .. )
    return Collections.emptyList();
}

결론

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

함수에서 nil vs 예외 처리
에러 케이스가 1가지인 경우는 nil, 여러 가지인 경우에는 예외 처리
https://stackoverflow.com/questions/31077613/optionals-vs-throwing-functions

0개의 댓글