티스토리 뷰

지난번 std::optional에 이어, 오늘은 C++17의 또 다른 강력한 무기인 std::variant에 대해 이야기해 보려고 한다. optional이 '값이 있거나 없거나'의 두 가지 상태를 다뤘다면, variant는 한 걸음 더 나아가 여러 가지 정해진 타입 중 하나가 될 수 있는 상태를 우아하게 처리한다.
예전엔 우리 어떻게 했더라? (feat. 공용체와 보이드 포인터)
하나의 변수가 여러 타입 중 하나를 가져야 하는 상황. 생각보다 흔하다. 네트워크 메시지는 '인증 요청'일 수도, '데이터 전송'일 수도 있다. UI 이벤트는 '마우스 클릭'일 수도, '키보드 입력'일 수도 있다. 이런 걸 처리하기 위해 예전에는 보통 이런 방법을 썼다.
union과enum의 조합: C 스타일의 전통적인 방식.union으로 여러 타입의 메모리를 겹쳐놓고,enum타입 변수로 지금 어떤 타입이 저장되어 있는지 수동으로 관리해야 한다. 타입 안전성을 전혀 보장할 수 없고, 생성자/소멸자가 있는 복잡한 타입을 다루기 까다롭다.void*포인터: 모든 포인터 타입으로 변환될 수 있는void*를 사용해 일단 데이터를 가리키게 하고, 별도의 타입 정보를 넘겨받아static_cast나dynamic_cast로 변환해서 쓴다. 위험하고, 런타임에 타입 오류가 발생하기 쉽다.- 상속을 이용한 다형성:
Event같은 부모 클래스를 만들고,MouseEvent,KeyEvent등을 파생 클래스로 만들어 처리한다. 가장 객체지향적인 방법이지만, 이 간단한 문제를 해결하기 위해 상속 구조를 만들고 가상 함수 테이블까지 동원하는 건 좀 과하게 느껴질 때가 많다. 동적 할당은 덤이다.
이런 방식들은 모두 코드를 복잡하게 만들고, 잠재적인 버그의 원인이 되곤 했다.
std::variant: 타입 안전성을 품은 공용체
std::variant는 이런 문제들을 해결하기 위해 등장한 '타입 안전한 공용체(Type-safe union)'다. 미리 지정된 여러 타입들 중 단 하나의 값만 저장할 수 있는 똑똑한 컨테이너다.
#include <variant>
#include <string>
// int, double, std::string 셋 중 하나의 값만 가질 수 있는 variant
std::variant<int, double, std::string> myVariant;
myVariant = 10; // 이제 myVariant는 int 값을 가짐
myVariant = 3.14; // 이제 double 값을 가짐
myVariant = "Hello"; // 이제 std::string 값을 가짐
union과 비슷해 보이지만, variant는 현재 어떤 타입의 값이 들어있는지 스스로 기억한다. 그리고 다른 타입으로 값을 읽으려고 시도하면 컴파일 에러나 런타임 예외를 발생시켜 실수를 막아준다.
실무에서 variant를 써보니... (feat. 네트워크 패킷 처리)
내가 variant의 강력함을 느꼈던 건, 여러 종류의 네트워크 패킷을 처리하는 로직을 구현할 때였다. 각 패킷은 종류도, 담고 있는 데이터 구조도 완전히 달랐다.
Before: 상속을 이용하던 시절
// 공통 부모 클래스
struct Packet {
virtual ~Packet() = default;
};
struct LoginRequest : Packet {
std::string username;
std::string password;
};
struct Message : Packet {
std::string content;
};
// 처리 함수
void processPacket(Packet* p) {
if (auto req = dynamic_cast<LoginRequest*>(p)) {
// 로그인 처리...
} else if (auto msg = dynamic_cast<Message*>(p)) {
// 메시지 처리...
}
delete p; // 동적 할당된 메모리 해제!
}
전형적인 상속 기반의 다형성 코드다. 나쁘진 않지만, 패킷 종류가 몇 개 안 되는데 상속 구조를 만들고, 가상 함수를 쓰고, dynamic_cast의 런타임 비용을 감수해야 한다. new/delete 관리도 귀찮다.
After: std::variant 도입 후
이 구조를 variant로 바꿨다. 각 패킷 구조체는 그대로 두고, 상속 관계만 제거했다.
#include <variant>
#include <vector>
struct LoginRequest {
std::string username;
std::string password;
};
struct Message {
std::string content;
};
// variant로 패킷 타입을 정의
using Packet = std::variant<LoginRequest, Message>;
// 처리 함수
void processPackets(const std::vector<Packet>& packets) {
for (const auto& p : packets) {
std::visit([](const auto& arg) {
// arg는 LoginRequest 또는 Message 타입
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, LoginRequest>) {
// 로그인 처리...
} else if constexpr (std::is_same_v<T, Message>) {
// 메시지 처리...
}
}, p);
}
}
실제 경험과 후기
variant와 visit를 함께 사용하니 신세계가 열렸다.
- 간결함과 안전성: 불필요한 상속 구조와
virtual키워드가 사라지니 코드가 훨씬 간결해졌다.dynamic_cast대신std::visit를 사용하니 컴파일 타임에 모든 타입을 처리하고 있는지 검사할 수 있어 훨씬 안전했다. 만약visit에서 특정 타입을 처리하는 로직을 빼먹으면 컴파일 에러가 발생한다! - 성능:
new/delete를 통한 동적 할당이 사라졌다.variant는 대부분 스택에 데이터를 저장하므로 캐시 효율이 좋고, 가상 함수 호출에 따른 오버헤드도 없다. - 유지보수의 편리함: 새로운 패킷(
LogoutRequest같은)을 추가하고 싶을 때, 그냥variant타입 목록에 추가하고visit람다 안에 처리 로직만 더해주면 끝이었다. 기존 클래스 구조를 건드릴 필요가 전혀 없었다.
std::visit와 템플릿 람다([](const auto& arg){...})의 조합은 처음엔 조금 낯설 수 있지만, 한번 익숙해지니 이보다 더 깔끔하고 안전한 방법이 없다는 생각이 들었다.
결론
std::variant는 서로 다른 타입의 데이터를 하나의 변수에 담아야 할 때, C++이 제공하는 가장 현대적이고 안전하며 효율적인 해결책이다. 특히 상속을 쓰기에는 부담스럽지만 여러 타입을 다뤄야 하는 경우에 variant는 최고의 선택이 될 것이다. 복잡한 상태 관리와 이벤트 처리 로직을 variant로 리팩토링해 보는 것을 강력히 추천한다.
#C++ #Cpp #Variant #ModernCpp #프로그래밍 #개발자 #실무팁 #GameDev #백엔드
'프로그래밍 > CC++' 카테고리의 다른 글
| C++ optional, '널 포인터'와 작별하는 가장 우아한 방법 (실무 후기) (0) | 2025.09.11 |
|---|---|
| 드라이버 예제 (0) | 2020.03.02 |
- Total
- Today
- Yesterday
- golang
- Frontend
- 재테크
- 부동산분석
- 프로그래밍
- 주식투자
- react
- Backend
- Java
- ios
- go
- ChatGPT
- Spring
- HTML
- AI
- MacOS
- reactjs
- Linux
- SWiFT
- 오리역
- CSS
- Python
- 개발자
- openai
- 생각
- 내집마련
- JavaScript
- 부동산
- 카카오톡
- 카톡업데이트
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |