Abseil Tip 181 StatusOr 값 접근하기

Tip of the Week #181: StatusOr<T> 값 접근하기

작성자: Michael Sheely
최초 작성일: 2020년 7월 9일
업데이트: 2020년 9월 2일
원문 링크: abseil.io/tips/181


StatusOr<Readability>: 선택하지 않아도 됩니다!

absl::StatusOr<T> 객체의 값을 사용할 때는 안전성, 명확성, 그리고 효율성을 염두에 두어야 합니다.

참고: 이 팁은 일반적인 사용 사례에 대해 권장 경로를 제공하려는 목적으로 작성되었습니다. 특수한 경우에 대해서는 여기의 권장사항과 이유를 검토하고 적절한 판단을 내리시기 바랍니다.


권장사항

StatusOr가 가진 값에 접근할 때는 ok() 호출을 통해 값이 있는지 확인한 후 operator* 또는 operator->를 사용하는 것이 가장 적합합니다.

// unique_ptr을 처리할 때 사용하는 동일한 패턴...
std::unique_ptr<Foo> foo = TryAllocateFoo();
if (foo != nullptr) {
  foo->DoBar();  // 값 사용
}

// optional 값에서도 동일한 방식...
std::optional<Foo> foo = MaybeFindFoo();
if (foo.has_value()) {
  foo->DoBar();
}

// StatusOr에서도 동일한 방식 사용
absl::StatusOr<Foo> foo = TryCreateFoo();
if (foo.ok()) {
  foo->DoBar();
}

StatusOr의 범위를 제한하려면 if 문 초기화 구문에서 선언하고, 조건문에서 ok()를 확인하세요. StatusOr를 바로 사용하는 경우에는 이렇게 범위를 제한하는 것이 일반적으로 권장됩니다(자세한 내용은 Tip #165을 참조하세요).

if (absl::StatusOr<Foo> foo = TryCreateFoo(); foo.ok()) {
  foo->DoBar();
}

StatusOr에 대한 배경 지식

absl::StatusOr<T> 클래스는 태그 유니언(tagged union)으로, 다음 두 가지 상황 중 하나를 나타냅니다:

  • T 타입 객체가 사용 가능하다.
  • 값이 없음을 나타내는 absl::Status 에러 상태 (!ok()).

absl::Statusabsl::StatusOr에 대한 자세한 내용은 Tip #76을 참고하세요.


안전성, 명확성, 효율성

StatusOr 객체를 스마트 포인터처럼 다루면 코드의 명확성을 유지하면서도 안전하고 효율적으로 작성할 수 있습니다. 이제 StatusOr 값에 접근하는 다른 방식과 권장 방식이 비교되는 이유를 살펴보겠습니다.


대안 접근 방식의 안전성 문제

absl::StatusOr<T>::value()는 어떨까요?

absl::StatusOr<Foo> foo = TryCreateFoo();
foo.value().DoBar();  // 동작이 빌드 모드에 따라 달라질 수 있습니다.

위 코드의 동작은 빌드 모드(특히 예외가 활성화되었는지 여부)에 따라 달라집니다1. 독자는 에러 상태가 프로그램을 종료시킬지 확실히 알 수 없습니다.

value() 메서드는 유효성 검사와 값 접근을 한 번에 수행하므로, 두 동작이 모두 필요한 경우에만 사용해야 합니다. 하지만, 상태가 이미 OK임을 알고 있다면 value()보다 operator*operator->를 사용하는 것이 적합합니다. 이는 코드가 의도를 더 명확하게 나타낼 뿐 아니라, value()가 유효성을 검사한 후 값을 반환하는 과정보다 적어도 동일하거나 더 효율적입니다.


동일 객체에 여러 이름 사용 피하기

StatusOr 객체를 스마트 포인터나 optional처럼 다루면, 동일 값을 참조하는 여러 변수를 생성하는 개념적으로 어색한 상황을 피할 수 있습니다. 또한, 이러한 방식으로 인해 발생할 수 있는 명명 문제와 auto의 과도한 사용도 방지할 수 있습니다.

// TryCreateFoo() 선언을 살펴보지 않으면, 독자는 타입을 바로 이해할 수 없습니다.
auto maybe_foo = TryCreateFoo();
// 암시적인 bool 사용이 문제를 더욱 복잡하게 만듭니다.
if (!maybe_foo) { /* foo 없음 처리 */ }
// 두 변수(maybe_foo, foo)가 동일 값을 나타냅니다.
Foo& foo = maybe_foo.value();

_or 접미어 피하기

StatusOr 변수의 유효성을 확인한 후 해당 값 타입을 바로 사용하는 방식은 변수 이름에 접두어나 접미어를 추가할 필요성을 줄여줍니다.

// 타입에서 이미 unique_ptr임을 알 수 있음. `foo`라는 이름이면 충분합니다.
std::unique_ptr<Foo> foo_ptr;

absl::StatusOr<Foo> foo_or = MaybeFoo();
if (foo_or.ok()) {
  const Foo& foo = foo_or.value();
  foo.DoBar();
}

단일 변수(StatusOr)만 사용할 경우, 접미어를 제거하고 변수 이름을 기본 값처럼 간단히 지정할 수 있습니다.

const absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
  MakeUseOf(*foo);
  foo->DoBar();
}

StatusOr 값 이동하기

absl::StatusOr<T>에서 T를 이동하려면 아래와 같은 코드를 작성할 수 있습니다:

absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
  ConsumeFoo(std::move(*foo));
}

하지만, StatusOr 자체에서 이동하는 방식이 더 낫습니다. 이렇게 하면 StatusOr 전체가 이동 후에는 사용되지 않을 것임을 독자(사람과 컴파일러 모두)에게 명확히 전달할 수 있습니다.

absl::StatusOr<Foo> foo = MaybeFoo();
if (foo.ok()) {
  ConsumeFoo(*std::move(foo));
}

해결책

absl::StatusOr 객체의 유효성을 검사하고(ok() 사용), operator* 또는 operator->를 사용해 값에 접근하는 방식은 가독성, 효율성, 안전성을 모두 만족합니다.

이 방법은 변수 이름 혼동 문제를 방지하고, 매크로 없이 간결하게 구현할 수 있습니다.

값에 접근하기 전에 유효성을 검증하는 코드는 접근 지점 근처에 배치하세요. 이는 독자가 값이 유효함을 쉽게 확인할 수 있도록 돕습니다.


  1. value() 함수의 문서에 따르면, 예외가 활성화된 경우 absl::BadStatusOrAccess를 던집니다(이를 잡을 수 있으므로 프로그램이 종료되지 않을 수도 있음). 예외가 비활성화된 경우에는 LOG(FATAL)로 프로그램이 충돌합니다.