자바/클린 코드

클린코드 7장 - 오류처리

끄적끄적 2022. 5. 24. 17:49

오류 코드보다 예외를 사용하라
오류를 코드로 관리하면, 함수를 호출하는 호출자 코드가 복잡해진다. 예외를 던지면, 실제 로직과 오류처리 로직이 뒤섞이지 않으니까 호출자 코드가 더 깔끔해진다.

// Bad
public class DeviceController {
...
public void sendShutDown() {
  DeviceHandle handle = getHandle(DEV1);
  // Check the state of the device
  if (handle != DeviceHandle.INVALID) {
    // Save the device status to the record field
    retrieveDeviceRecord(handle);
    // If not suspended, shut down
    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());
  }
}
..

아래는 예외를 사용한 코드

// Good
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문부터 작성하라
예외가 발생할 부분은 try문으로 에러를 먼저 정의해놓고, 해당 테스트를 작성하면서 TDD스타일로 소스를 완성해나가는게 좋다.

Unchecked Exception을 사용하라
checked Exception은 java가 나온 과거에는 좋은 아이디어였으나, 현재에는 맞지 않다. OCP원칙도 위반한다. 하위 코드에서 새로운 예외를 던지는 등 코드를 변경하면 catch블록이 있는 상위 코드까지 전부 고쳐야 한다. 신규로 Checked Exception은 만들지 말고, 라이브러리의 Checked Exception은 받아서 더 구체적인 Unchecked Exception으로 포장해서 던지는 걸 고려하라는 의미로 보인다. ( 참고 : https://blog.benelog.net/1901121, https://cheese10yun.github.io/checked-exception/)

(참고 :   https://www.nextree.co.kr/p3239/ )

예외에 의미를 제공하라
예외가 발생한 이유와 좀더 구체적인 Exception을 제공하라.

호출자를 고려해 예외 클래스를 정의하라
예외에 대해 처리방법이 동일하면 하나의 예외클래스로 처리하는게 낫다. 한 예외는 잡아내고 다른 예외는 무시해도 괜찮은 경우리면 예외 클래스를 분리한다.
외부API의 경우는 Wrapper 클래스로 감싸서 예외를 재정의 하라(동작이 같은거끼리 묶어서)

// Bad
// 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 {
  ...
}
// Good
// ACME 클래스를 LocalPort 클래스로 래핑해 new ACMEPort().open() 메소드에서 던질 수 있는 exception들을 간략화

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

정상 흐름을 정의하라
catch문에서 별도 업무로직을 넣을 거면, 그것은 예외로 처리할 것이 아니라, special case를 정의하여 처리하고 예외는 빼는게 낫다. 예외적인 상황을 캡슐화해서 호출자에서는 예외상황을 처리할 필요가 없어진다.

null을 반환하지 마라.
null을 반환하는 코드는 호출자에게 항상 null을 체크해야 하는 문제를 떠넘긴다. 외부API가 null을 반환한다면 wrapper 메서드를 구현해서 예외를 던지거나 special case 처리를 하면 된다. null을 반환하기 보다는 Collections.emptyList()와 같이 빈 리스트를 반환하면 된다.

null을 전달하지 마라.
null을 리턴하는 것도 나쁘지만, null을 메서드로 넘기는 것은 더 나쁘다. null을 메서드의 파라미터로 넣어야 하는 API를 사용하는 경우가 아니면 null을 넘기지 마라. 

반응형