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::Status
와 absl::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->
를 사용해 값에 접근하는 방식은 가독성, 효율성, 안전성을 모두 만족합니다.
이 방법은 변수 이름 혼동 문제를 방지하고, 매크로 없이 간결하게 구현할 수 있습니다.
값에 접근하기 전에 유효성을 검증하는 코드는 접근 지점 근처에 배치하세요. 이는 독자가 값이 유효함을 쉽게 확인할 수 있도록 돕습니다.