티스토리 뷰

오랜만에 C++ 관련 글을 쓰는 것 같다. 오늘은 C++17에 등장한 이후로 나의 '최애' 기능 중 하나가 된 std::optional에 대해 이야기해 보려고 한다. 특히 현업에서 골치 아팠던 문제를 optional이 얼마나 우아하게 해결해 줬는지, 실제 경험을 바탕으로 풀어볼까 한다.
예전엔 우리 어떻게 했더라? (feat. 널 포인터와 마법의 숫자)
함수에서 '값이 없음' 또는 '실패'를 어떻게 표현했는지 돌이켜보자. 아마 대부분 비슷할 것이다.
- 포인터를 반환하며, 실패 시
nullptr반환: 가장 흔한 방식. 하지만 호출하는 쪽에서 잊지 않고 꼭 널 체크를 해줘야만 한다. 이걸 깜빡하면? 바로 런타임 에러와 함께 프로그램이 뻗어버린다. (그리고 이런 버그는 꼭 새벽에 터진다...) - '마법의 숫자'나 특정 값으로 약속: 가령 ID를 찾는 함수에서 실패 시
-1을 반환한다거나, 객체를 반환할 때isEmpty()같은 플래그를 가진 '빈 객체'를 반환하는 식이다. 이것도 나름의 방법이지만, 팀원 모두가 이 '약속'을 기억하고 지켜야 한다는 부담이 있고, 코드가 직관적이지 않게 된다.
나 역시 수많은 널 포인터 버그와 싸웠고, 동료의 코드를 보며 "여기서 -1이 정확히 무슨 의미였죠?"라고 물어본 경험도 많다. 항상 뭔가 찜찜한 구석이 있었다.
std::optional: '있거나, 혹은 없거나'
std::optional은 이런 문제들을 해결하기 위해 C++17 표준에 도입된 기능이다. 단어 뜻 그대로, 값이 '선택적으로' 들어있을 수 있는 래퍼(Wrapper) 클래스다.
핵심은 아주 간단하다. T 타입의 객체를 가질 수도 있고, 아무것도 가지지 않을 수도 있는 상태를 하나의 타입으로 명확하게 표현하는 것이다.
#include <optional>
std::optional<std::string> findUserName(int userID) {
if (/* 유저를 데이터베이스에서 찾았다면 */) {
return "worni"; // 값이 있으면 값을 반환
}
return std::nullopt; // 값이 없으면 std::nullopt를 반환
}
함수 시그니처 std::optional<std::string>만 봐도 이제 이 함수가 문자열을 반환할 수도 있지만, 아닐 수도 있다는 사실을 컴파일러와 개발자 모두에게 명확하게 알려준다. 더 이상 nullptr인지 아닌지, 반환값이 -1인지 아닌지 걱정할 필요가 없다.
실무에서 optional을 써보니... (feat. 유저 정보 조회)
내가 optional의 진가를 제대로 느꼈던 것은 유저 정보를 관리하는 백엔드 서비스에서였다. 특정 ID의 유저 정보를 캐시나 DB에서 조회하는 함수가 있었다.
Before: nullptr를 사용하던 시절
struct User {
int id;
std::string name;
// ... 기타 정보
};
// 실패 시 nullptr을 반환하는 함수
User* findUser(int userID) {
// ... DB 조회 로직 ...
if (/* 유저를 찾았다면 */) {
User* user = new User{...};
return user;
}
return nullptr;
}
// 호출부
void printUserName(int id) {
User* user = findUser(id);
if (user != nullptr) { // 잊지 말자! 널 체크!
std::cout << user->name << std::endl;
delete user; // 잊지 말자! 메모리 해제!
} else {
std::cout << "User not found" << std::endl;
}
}
위 코드의 문제점은 명확하다. if (user != nullptr) 체크를 깜빡하는 순간, 프로그램은 크래시로 이어진다. 동적 할당까지 했다면 메모리 누수 문제까지 신경 써야 한다. 아주 위험하고 피곤한 코드다.
After: std::optional 도입 후
이 코드를 optional을 사용해 아래와 같이 리팩토링했다.
#include <optional>
struct User {
int id;
std::string name;
// ... 기타 정보
};
// 값이 없을 수 있음을 명시적으로 표현
std::optional<User> findUser(int userID) {
// ... DB 조회 로직 ...
if (/* 유저를 찾았다면 */) {
return User{...}; // 객체를 값으로 반환
}
return std::nullopt;
}
// 호출부
void printUserName(int id) {
std::optional<User> userOpt = findUser(id);
if (userOpt) { // optional은 bool 타입처럼 바로 체크 가능
// .value() 또는 * 연산자로 값에 접근
std::cout << userOpt->name << std::endl;
} else {
std::cout << "User not found" << std::endl;
}
}
실제 경험과 후기
코드가 어떻게 바뀌었는지 보이는가? optional을 도입하고 나서 얻은 장점은 기대 이상이었다.
- 코드의 명확성:
std::optional<User>라는 반환 타입만으로 "이 함수는 유저를 못 찾을 수도 있다"는 사실이 명확해졌다. 동료 개발자가 이 함수를 사용할 때 더 이상 내부 구현을 보거나 문서를 뒤지지 않아도 됐다. - 런타임 에러 방지:
nullptr가 사라지니 널 포인터 역참조(dereferencing)로 인한 크래시가 원천적으로 봉쇄되었다. 컴파일 타임에 실수를 방지할 수 있는 구조가 된 것이다. - 편의 기능:
userOpt.value_or(defaultUser)처럼 값이 없을 때 사용할 기본 객체를 간단하게 지정할 수 있는 것도 매우 편리했다. - 자원 관리: 더 이상
new와delete를 수동으로 관리할 필요가 없어졌다.optional은 값 자체를 저장하므로 훨씬 안전하고 간결하다.
처음에는 optional 객체로 감싸는 게 어색하고 코드가 조금 길어지는 느낌도 들었지만, 한두 번 써보고 나니 그 안정성과 명확성이 주는 이점이 훨씬 크다는 것을 깨달았다. 이제는 '값이 없을 수 있는' 상황이라면 무조건 optional을 가장 먼저 고려한다.
결론
C++17 이상을 사용하고 있다면, 그리고 함수에서 값이 부재하는 경우를 표현해야 한다면 std::optional의 사용을 강력하게 추천한다. 널 포인터와 작별하고, 더 안전하고 표현력 높은 코드를 작성하는 가장 우아한 방법이라고 자신 있게 말할 수 있다.
#C++ #Cpp #Optional #ModernCpp #프로그래밍 #개발자 #실무팁 #후기
'프로그래밍 > CC++' 카테고리의 다른 글
| C++ variant, 상속 없이 다형성 흉내 내기 (실무 후기) (0) | 2025.09.11 |
|---|---|
| 드라이버 예제 (0) | 2020.03.02 |
- Total
- Today
- Yesterday
- 카톡업데이트
- go
- 오리역
- react
- 부동산
- Frontend
- ChatGPT
- Linux
- 개발자
- CSS
- openai
- golang
- Java
- MacOS
- Backend
- AI
- 카카오톡
- Python
- ios
- 주식투자
- HTML
- 부동산분석
- JavaScript
- 프로그래밍
- 재테크
- 내집마련
- 생각
- SWiFT
- reactjs
- Spring
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |