Abseil Tip 171 Sentinel 값 피하기

주간 팁 #171: Sentinel 값 피하기

원래 TotW #171로 2019년 11월 8일 게시됨
작성자: Hyrum Wright
2020-04-06 업데이트됨

빠른 링크: abseil.io/tips/171

Sentinel 값은 특정 컨텍스트에서 특별한 의미를 가지는 값입니다. 예를 들어, 다음과 같은 API를 생각해봅시다:

// 계좌 잔액을 반환하거나 계좌가 닫힌 경우 -5를 반환합니다.
int AccountBalance();

int의 모든 값은 AccountBalance의 유효한 반환 값으로 문서화되어 있지만, -5는 예외입니다. 직관적으로, 이것은 약간 이상해 보입니다. 호출자가 -5에 대해 명시적으로만 확인해야 하는지, 아니면 모든 음수 값이 “계좌 닫힘” 신호로 신뢰할 수 있는지 명확하지 않습니다. 시스템이 음수 잔액을 지원해야 하거나 API가 음수 값을 반환하도록 조정되면 어떻게 될까요?

Sentinel 값을 사용하는 것은 호출 코드의 복잡성을 증가시킵니다. 만약 호출자가 엄격하다면, Sentinel 값을 명시적으로 확인합니다:

int balance = AccountBalance();
if (balance == -5) {
  LOG(ERROR) << "account closed";
  return;
}
// 여기서 `balance`를 사용합니다.

일부 호출자는 문서에 명시된 것보다 더 넓은 범위의 값을 확인할 수도 있습니다:

int balance = AccountBalance();
if (balance <= 0) {
  LOG(ERROR) << "where is my account?";
  return;
}
// 여기서 `balance`를 사용합니다.

그리고 일부 호출자는 Sentinel 값을 완전히 무시하고, 실제로 발생하지 않는다고 가정할 수도 있습니다:

int balance = AccountBalance();
// 여기서 `balance`를 사용합니다.

Sentinel 값의 문제점

위의 예는 Sentinel 값을 사용할 때 발생하는 일반적인 문제를 보여줍니다. 그 외에도 다음과 같은 문제가 있습니다:

  • 서로 다른 시스템이 서로 다른 Sentinel 값을 사용할 수 있습니다. 예를 들어, 하나의 음수 값, 모든 음수 값, 무한 값, 또는 임의의 값을 사용할 수 있습니다. 특별한 값을 전달하는 유일한 방법은 문서화를 통해서입니다.
  • Sentinel 값은 여전히 해당 타입의 유효 값 범위에 속하므로, 호출자와 피호출자는 타입 시스템에 의해 값이 유효하지 않을 수 있음을 강제받지 않습니다. 코드와 주석이 일치하지 않을 경우, 둘 다 대개 잘못됩니다.
  • Sentinel 값은 인터페이스의 진화를 제한합니다. 특정 Sentinel 값이 언젠가는 해당 시스템에서 유효한 값이 될 수 있기 때문입니다.
  • 한 시스템의 Sentinel 값이 다른 시스템에서는 유효한 값이 될 수 있어, 여러 시스템 간의 인터페이스 작업 시 인지 부하와 코드 복잡성이 증가합니다.

특정 Sentinel 값을 확인하지 않는 것은 일반적인 버그입니다. 최상의 경우, 확인되지 않은 Sentinel 값을 사용하는 것은 런타임에 시스템을 즉시 충돌시킬 것입니다. 더 자주 발생하는 경우는, 확인되지 않은 Sentinel 값이 시스템을 통해 계속 전파되어 잘못된 결과를 초래하는 것입니다.


Sentinel 값 대신 std::optional 사용하기

특별한 값을 사용하는 대신 std::optional을 사용하여 유효하지 않거나 사용할 수 없는 정보를 나타내세요.

// 계좌 잔액을 반환하거나 계좌가 닫힌 경우 std::nullopt를 반환합니다.
std::optional<int> AccountBalance();

AccountBalance()의 새 버전을 호출하는 호출자는 이제 반환된 값 내부에 잠재적인 잔액이 있는지 명시적으로 확인해야 하며, 결과가 유효하지 않을 수 있음을 신호로 전달합니다. 추가 문서화가 없으면, 호출자는 이 함수가 반환할 수 있는 유효한 int 값을 특정 Sentinel 값 없이 가정할 수 있습니다. 이 단순화는 호출 코드의 의도를 더 명확히 합니다.

std::optional<int> balance = AccountBalance();

if (!balance.has_value()) {
  LOG(ERROR) << "Account doesn't exist";
  return;
}
// 여기서 `*balance`를 사용합니다.

다음에 시스템 내에서 Sentinel 값을 사용하고 싶은 유혹이 생긴다면, 대신 적절한 std::optional을 사용하는 것을 강력히 고려해보세요.


관련 자료

  • std::optional을 사용하여 값을 매개변수로 전달하는 방법에 대한 추가 정보는 ::optionalstd::unique_ptr` 중 어떤 것을 사용할지 결정하는 데 도움이 필요하다면, 을 참조하세요.