나만의 작은 도서관

[TIL][C++] 250731 MMO 서버 개발 70일차: C++에서 JSON 파싱하는 라이브러리: nlohmann, 람다식의 캡처에 shared_ptr을 넘겨주는 상황에서 복사? 레퍼런스? 본문

Today I Learn

[TIL][C++] 250731 MMO 서버 개발 70일차: C++에서 JSON 파싱하는 라이브러리: nlohmann, 람다식의 캡처에 shared_ptr을 넘겨주는 상황에서 복사? 레퍼런스?

pledge24 2025. 8. 1. 02:07
주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다. 

C++에서 JSON을 파싱하는 라이브러리: nlohmann

  • 여러 종류의 서버들과 통신하거나 외부 메모리에 데이터를 저장할 때 어디에서도 사용 가능하도록 XML이나 JSON과 같은 데이터 형식을 사용한다. 이 중 이번에 사용하게 될 데이터 형식은 JSON으로, AuthServer가 넣은 AccessToken의 값을 가져올 때 사용하려고 한다.

 

왜 nlohmann을 선택했는가

  • c++에서 JSON 파서로 사용하는 대표적인 라이브러리로 크게 2가지가 있는데, 바로 위에서 설명한 nlohmann과 rapidjson이다.
  • 두 라이브러리의 장단점은 극명한데, nlohmann은 쉬운 문법으로 json 데이터 형식을 파싱 할 수 있어 빠르게 적용할 수 있다는 장점을 가지지만, 성능적인 면에서 좋지 않은, 즉, 오버헤드가 있는 반면, rapidjson은 문법의 난도가 있는 편이어서 적용하는데 어려움이 있지만, 성능적인 면에서 nlohmann보다 우수하다.
  • 즉, 성능을 우선시한다면 rapidjson, 빠른 적용이 중요하다면 nlohmann을 사용하는 것이 적합하다. 본인의 경우, json 파싱 작업의 빈도가 적고, 핵심적인 부분이 아니라고 판단해 쓰기 쉬운 nlohmann을 사용하기로 결정했다.

 

nlohmann의 좋은 점: 필요한 파일은 단 하나. “json.hpp”

  • nlohmann은 다른 라이브러리와 다르게 CMake로 돌린 결과물을 직접 빌드하거나 할 필요 없다. 왜냐하면 따로 라이브러리가 없기 때문. 그저 json.hpp를 다운로드하고 프로젝트 폴더 내에 때려 박아 넣는 방식만으로도 사용이 가능하다. 이러한 점 때문에 라이브러리를 적용하는데 애를 먹을 일이 없다는 것이 너무나도 좋다.
  • 그럼에도 개인적으론 기존 라이브러리 적용 방식에서처럼 포함 경로 설정 및 라이브러리 폴더에서 따로 관리하는 것이 좋은 것 같다. 1) json.hpp 내부에서 nlohmann이라는 폴더 경로를 사용하는 것도 있고, 2) 무엇보다 라이브러리 파일임을 명시할 수 있기 때문이다.

 

nlohmann/json.hpp 사용법

  • 기본적으로 nlohmann은 네임스페이스가 있다. “std::”처럼 nlohmann::json을 Json객체를 사용할 때마다 앞에 적어주는 것도 좋지만, 개인적으론 지저분하다고 생각하기에 아래와 같이 using으로 재정의하는 것을 추천한다.
#include "nlohmann/json.hpp"
using Json = nlohmann::json;

 

 

사용법 예시

// C++에서 문자열 리터럴 앞에 붙는 R 접두사는 원시 문자열 리터럴을 의미.
// 원시 문자열 리터럴은 백슬래시('\\\\')와 같은 이스케이프 문자를 일반 문자로 처리
// -> 백슬래시를 한 번만 입력해도 됨.
// L과 함께 쓰려면 LR로 하면됨. (순서를 바꿔도 똑같이 동작핮지만 표준 관례가 아님)
// R은 접두사 중 가장 뒤에 온다.

// Parse
string json_string = R"({"name": "John Doe", "age": 30, "city": "New York"})";
Json j = Json::parse(json_string); // String To Json

// Read
string name = j["name"];
int age = j["age"];
string city = j["city"];

cout << "Name: " << name << endl;
cout << "Age: " << age << endl;
cout << "City: " << city << endl;

//Write
Json user;
user["name"] = "Jane Doe";
user["age"] = 25;
user["city"] = "Los Angeles";

string jsonString = user.dump(); // Json To String
cout << "User JSON: " << jsonString << endl;

 

 

언리얼에서 FString → string으로 변환하기

  • 언리얼에서 사용하는 문자열 타입 “FString”은 프로토버프의 string 타입의 속성 인자로 넣게 되면 오류가 발생한다. 따라서 FString을 string 타입으로 바꿔줘야 하는데, 이에 대한 언리얼 매크로함수는 “TCHAR_TO_UTF8”이며, 아래와 같이 사용한다.
Protocol::C_LOGIN Pkt;
std::string tokenStr = TCHAR_TO_UTF8(*_token);
Pkt.set_accesstoken(tokenStr);

 

 

람다식의 캡처에 shared_ptr을 넘겨주는 상황에서 복사? 레퍼런스?

  • 람다식을 사용하다 보면, 캡처에 shared_ptr타입을 넘겨줘야 하는 경우가 있다. 이때 캡처모드로 복사 또는 레퍼런스 방식으로 넘겨줄 수 있는데, 어떤 방법이 적합할까? 둘 다 사용해도 괜찮을까?

 

어지간하면 복사 방식이 안전하다.

  • shared_ptr를 복사 방식이 아닌 레퍼런스 방식으로 넘겨줬을 때의 문제점은 “레퍼런스 원본이 선언된 스코프를 벗어나면 원본이 사라짐과 동시에 레퍼런스도 사라진다”는 것이다.

 

레퍼런스의 기본 동작 방식

// 레퍼런스의 주의사항: 원본 변수가 레퍼런스보다 먼저 사라지는 경우
int& getReference() {
    int local = 42;  // 지역 변수
    return local;    // 위험! 지역 변수에 대한 레퍼런스 반환
}  // local이 여기서 소멸됨

int main() {
    int& ref = getReference();  // ref는 이미 소멸된 객체를 참조
    // ref 사용 시 undefined behavior
}
  • 위 코드에서처럼 레퍼런스는 원본이 소멸되면 레퍼런스 변수 또한 같이 사라진다. 이러한 동작 방식은 레퍼런스가 존재하는 상태로 원본 변수가 먼저 사라질 수 있는 람다식에서 문제가 생길 수 있다.

 

레퍼런스 + 람다식 문제 발생 예제

// shared_ptr을 선언하고, 이를 레퍼런스 방식으로 람다가 캡처해갔다면,
// shared_ptr을 선언한 함수의 스코프를 벗어난 다음에 람다를 호출하면, 람다가 캡처해간 변수는 dangling reference가 되어 람다식은 정상적으로 실행되지 않는다
#include <memory>
#include <functional>

std::function<void()> createLambda() {
    auto shared_ptr_var = std::make_shared<int>(42);  // shared_ptr 생성
    
    // 레퍼런스로 캡처 (&shared_ptr_var)
    auto lambda = [&shared_ptr_var]() {
        std::cout << *shared_ptr_var << std::endl;  // shared_ptr_var 사용
    };
    
    return lambda;
}  // shared_ptr_var이 여기서 소멸됨!

int main() {
    auto lambda = createLambda();
    lambda();  // 위험! shared_ptr_var는 이미 소멸된 상태
    return 0;
}

 

 

결론

  • 레퍼런스 방식으로 shared_ptr을 캡처해 가면 원본보다 레퍼런스의 수명이 긴 경우, dangling reference가 되어 정의되지 않은 동작(Undefined Behavior)을 초래하므로, 어지간하면 원본 shared_ptr와 독립적으로 운영되는 복사 방식으로 캡처하는 것이 안전하다!