실무 C++ 코드를 작성하다 보면 “값이 없을 수도 있음”을 표현해야 하는 순간이 자주 나옵니다.
검색 결과가 없을 수도 있고, 설정 파일에 값이 없을 수도 있고, 문자열 파싱이 실패할 수도 있습니다.
이때 -1, 빈 문자열, nullptr 같은 값으로 없는 상태를 대신 표현하면 코드가 점점 애매해집니다.
std::optional은 값이 있을 수도 있고 없을 수도 있다는 의도를 타입으로 표현하는 C++17 도구입니다.
이번 글에서는 std::optional이 왜 필요한지, sentinel value와 무엇이 다른지, 그리고 실무에서 어떤 기준으로 사용하면 좋은지 코드 예제로 정리해보겠습니다.

1. std::optional은 무엇인가?
std::optional<T>는 T 타입의 값을 가질 수도 있고, 아무 값도 갖지 않을 수도 있는 wrapper입니다.
값이 있는 상태를 engaged state라고 볼 수 있고, 값이 없는 상태는 std::nullopt로 표현합니다.
중요한 점은 “없을 수도 있음”이 주석이나 관례가 아니라 함수의 return type에 드러난다는 것입니다.
| 표현 방식 | 예시 | 문제 또는 의미 |
| sentinel value | -1, 0, 빈 문자열 | 정상 값과 실패 값을 사람이 구분해야 함 |
| nullptr | 포인터 반환 | 객체 소유권과 없음 상태가 섞일 수 있음 |
| bool + out parameter | 성공 여부와 결과를 따로 전달 | 호출 코드가 길어지고 결과 흐름이 흩어짐 |
| std::optional<T> | 값 또는 std::nullopt 반환 | 없을 수 있다는 의도가 타입에 직접 드러남 |
std::optional은 예외 상황을 숨기는 도구가 아닙니다.
값이 없는 것이 정상적인 분기일 때, 그 사실을 명확하게 표현하기 위한 도구입니다.
2. 왜 sentinel value보다 나은가?
sentinel value는 특정 값을 약속해서 실패나 없음 상태를 표현하는 방식입니다.
예를 들어 사용자의 점수를 찾지 못하면 -1을 반환하는 함수를 생각해볼 수 있습니다.
처음에는 간단해 보이지만 시간이 지나면 문제가 생깁니다.
- -1이 정말 불가능한 값인지 모든 호출자가 알아야 한다.
- 나중에 점수 범위가 바뀌면 sentinel 규칙도 같이 깨질 수 있다.
- 반환값만 보면 “값이 없음”인지 “정상 값”인지 타입으로 구분되지 않는다.
- 호출자가 체크를 빼먹어도 컴파일러가 의도를 알려주기 어렵다.
std::optional을 사용하면 함수 시그니처가 먼저 말해줍니다.
이 함수는 항상 int를 반환하는 것이 아니라, int가 없을 수도 있다는 사실이 std::optional<int>에 담깁니다.
그래서 호출자는 값을 사용하기 전에 has_value, if 문, value_or 같은 접근 방식을 선택해야 합니다.

3. 상태 흐름을 그림으로 이해하기
std::optional은 값이 없는 상태에서 시작해 값을 갖고, 다시 reset으로 비어 있는 상태가 될 수 있습니다.

개념적으로는 다음과 같은 상태를 떠올리면 됩니다.
- optional state
std::optional<int> score;
empty 상태
[ has_value: false ][ int 객체 없음 ]
score = 92;
has value 상태
[ has_value: true ][ int 객체: 92 ]
score.reset();
empty 상태
[ has_value: false ][ int 객체 없음 ]
여기서 핵심은 std::optional이 포인터처럼 어딘가를 가리키는 개념이 아니라는 점입니다.
std::optional<T>는 T 객체가 활성화되어 있는지 여부를 관리하고, 값이 있을 때만 내부 T 객체의 생명주기가 시작됩니다.
4. C++17 코드로 사용해보기
다음 예제는 이름으로 점수를 찾는 함수입니다.
점수를 찾으면 int 값을 반환하고, 찾지 못하면 std::nullopt를 반환합니다.
#include <iostream>
#include <optional>
#include <string_view>
#include <utility>
#include <vector>
std::optional<int> find_score(std::string_view name) {
const std::vector<std::pair<std::string_view, int>> scores = {
{"kim", 92},
{"lee", 85},
{"park", 78}
};
for (const auto& [user, score] : scores) {
if (user == name) {
return score;
}
}
return std::nullopt;
}
void print_score(std::string_view name) {
const auto score = find_score(name);
if (score.has_value()) {
std::cout << name << ": " << *score << '\n';
return;
}
std::cout << name << ": not found\n";
}
int main() {
print_score("kim");
print_score("choi");
const int default_score = find_score("choi").value_or(0);
std::cout << "default score: " << default_score << '\n';
}
실행 결과
kim: 92
choi: not found
default score: 0
find_score의 return type만 봐도 점수가 없을 수 있다는 사실이 드러납니다.
호출자는 값을 사용하기 전에 has_value로 확인하거나, value_or로 기본값을 정해야 합니다.
이렇게 하면 “찾지 못하면 -1” 같은 숨은 규칙을 호출자에게 암묵적으로 떠넘기지 않아도 됩니다.
5. 코드에서 봐야 할 핵심
std::optional을 사용할 때 자주 보는 표현은 다음과 같습니다.
| 표현 | 의미 | 주의할 점 |
| return score | 값이 있는 optional을 반환 | T에서 optional<T>로 변환되어 저장됨 |
| return std::nullopt | 값이 없는 optional을 반환 | 실패 이유까지 담지는 않음 |
| has_value() | 값이 있는지 확인 | 값을 꺼내기 전에 확인하는 기본 방식 |
| *score | 내부 값을 참조 | 값이 있다는 확인 이후에 사용해야 함 |
| value_or(0) | 값이 없으면 기본값 사용 | 기본값을 만드는 비용과 의미를 확인해야 함 |
| reset() | 값을 제거하고 empty 상태로 전환 | 내부 T 객체의 생명주기가 끝남 |
value()도 사용할 수 있지만, 값이 없는 optional에서 value()를 호출하면 std::bad_optional_access 예외가 발생합니다.
그래서 일반적인 분기에서는 if 문이나 value_or를 먼저 고려하는 편이 읽기 쉽습니다.
6. 객체 생명주기와 메모리 관점
std::optional은 객체 생명주기 관점에서도 의미가 있습니다.
값이 없는 optional은 내부 T 객체가 활성화되어 있지 않은 상태입니다.
값을 대입하면 T 객체가 생성되고, reset하거나 optional 자체가 소멸되면 내부 T 객체도 함께 정리됩니다.
| 관점 | 설명 |
| heap allocation | std::optional을 쓴다고 해서 별도 heap allocation이 자동으로 생기는 것은 아님 |
| 크기 | T를 저장할 공간과 값 존재 여부를 나타내는 상태 정보가 필요함 |
| 복사 비용 | optional을 복사하면 값이 있는 경우 내부 T도 복사될 수 있음 |
| 이동 비용 | T가 move를 지원하면 optional도 그 비용 구조를 따름 |
| 다형성 | optional은 구체 타입 T를 담는 도구이므로 polymorphism 목적이면 pointer 계열을 검토해야 함 |
즉, std::optional은 nullable pointer의 단순한 대체물이 아닙니다.
동적 객체를 소유해야 한다면 std::unique_ptr이나 std::shared_ptr이 더 맞을 수 있고, 단지 결과 값이 없을 수 있음을 표현하려면 std::optional이 더 자연스럽습니다.
7. 자주 하는 오해
std::optional은 편리하지만 모든 “없음” 문제를 해결하는 만능 타입은 아닙니다.
- 오해 1. 실패 이유도 optional에 담긴다.
std::optional은 값이 없음만 표현합니다. 실패 이유, 에러 코드, 진단 메시지가 필요하면 별도의 Result 타입이나 C++23의 std::expected 같은 구조를 검토해야 합니다. - 오해 2. 빈 문자열과 optional string은 같다.
빈 문자열은 값이 있지만 길이가 0인 상태입니다. std::optional<std::string>은 문자열 값 자체가 없는 상태를 추가로 표현합니다. - 오해 3. optional이면 무조건 안전하다.
값이 없는 optional에서 무심코 *opt를 사용하면 undefined behavior가 될 수 있습니다. 값 확인 규칙은 여전히 필요합니다. - 오해 4. 참조도 optional로 감싸면 된다.
표준 std::optional은 reference type을 직접 담지 않습니다. 참조 의미가 필요하면 pointer, std::reference_wrapper, 또는 API 설계를 다시 검토해야 합니다.
특히 optional을 반환했는데 호출자가 매번 value()만 호출한다면, 타입은 좋아졌지만 사용 방식은 여전히 위험할 수 있습니다.
optional을 쓰는 목적은 체크를 귀찮게 만드는 것이 아니라, 체크가 필요한 지점을 코드에 드러내는 것입니다.
8. 실무에서는 어떻게 볼까?
std::optional은 “없을 수 있음”이 정상 흐름일 때 가장 잘 맞습니다.
예를 들어 캐시 조회 실패, optional 설정값, 검색 결과 없음, 파싱 결과 없음, 조건부 계산 결과 같은 상황입니다.
반대로 파일 권한 오류, 네트워크 장애, 데이터 손상처럼 원인과 대응이 중요한 실패라면 optional 하나로 뭉개지 않는 편이 좋습니다.
| 상황 | 권장 표현 |
| 찾는 값이 없을 수 있음 | std::optional<T> |
| 실패 이유를 호출자에게 알려야 함 | Result type, error code, exception, std::expected 검토 |
| 소유권이 있는 동적 객체가 필요함 | std::unique_ptr 또는 std::shared_ptr |
| 읽기 전용으로 잠깐 빌려 쓰는 값 | const reference 또는 pointer |
| 빈 결과와 미실행 상태를 구분해야 함 | std::optional<std::vector<T>> 같은 구조도 가능 |
함수 return type에 std::optional이 보이면, 리뷰하는 사람은 바로 질문할 수 있습니다.
“값이 없는 것이 정상적인 경우인가?”, “호출자가 실패 이유를 알 필요는 없는가?”, “기본값을 쓰면 의미가 깨지지 않는가?” 같은 질문입니다.
이 질문에 답이 명확하면 optional은 API를 읽기 쉽게 만들어줍니다.
9. 정리
std::optional은 C++17에서 값이 없을 수 있는 결과를 타입으로 표현하는 도구입니다.
sentinel value보다 의도가 명확하고, 호출자가 값 확인을 의식하게 만듭니다.
값이 없다는 사실만 필요할 때는 optional이 잘 맞지만, 실패 이유가 중요하면 다른 에러 표현을 고려해야 합니다.
optional은 pointer의 대체물이 아니라 값의 존재 여부를 표현하는 wrapper입니다.
실무에서는 검색, 파싱, 설정, 캐시 조회처럼 없음이 정상 흐름인 API에 우선 적용해볼 수 있습니다.
std::optional을 잘 쓰는 기준은 “값이 없는 것이 정상적인 분기인가?”라는 질문에 명확히 답할 수 있는지입니다.
'C++ > Concepts' 카테고리의 다른 글
| C++ unordered_map bucket과 rehash 이해하기 (0) | 2026.07.02 |
|---|---|
| std::shared_ptr과 std::weak_ptr 제대로 이해하기 (0) | 2026.06.22 |
| std::unique_ptr 제대로 이해하기 (0) | 2026.06.18 |
| RAII로 자원 관리 이해하기 (0) | 2026.06.15 |
| C++ std::move는 진짜 객체를 이동시킬까? (0) | 2026.06.12 |