C++ JSON 처리 | nlohmann/json으로 파싱과 생성하기 [#27-2]

C++ JSON 처리 | nlohmann/json으로 파싱과 생성하기 [#27-2]

이 글의 핵심

C++ JSON 처리에 대한 실전 가이드입니다. nlohmann/json으로 파싱과 생성하기 [#27-2] 등을 예제와 함께 설명합니다.

들어가며: JSON 파싱이 복잡해요

문제 시나리오

C++에서 JSON을 다루려다 보면 이런 상황을 자주 마주칩니다:

  • REST API 응답을 받았는데, {"data": [{"id": 1, "name": "Alice"}]} 같은 문자열을 어떻게 구조화된 데이터로 바꾸지? 수동으로 strstr이나 정규식으로 파싱하면 버그가 나기 쉽고, 중첩 객체·배열·이스케이프 문자 처리가 지옥입니다.
  • 설정 파일(config.json)을 로드해서 port, host, timeout 같은 값을 읽어야 하는데, C++ 표준에는 JSON 파서가 없어요. 서드파티 라이브러리를 쓰더라도 빌드 설정이 복잡하거나 API가 직관적이지 않습니다.
  • 타입 안전성이 걱정돼요. j["age"]가 문자열 "30"인데 int로 읽으면? 키가 없는데 j["optional"]로 접근하면? 런타임 크래시나 예기치 않은 동작이 발생합니다.
  • 커스텀 구조체를 JSON으로 직렬화/역직렬화하고 싶은데, 수동으로 필드마다 j["name"] = obj.name을 반복하는 건 번거롭고 실수하기 쉽습니다.

추가 문제 시나리오

시나리오 1: 외부 API 응답 파싱
REST API에서 {"status": "ok", "data": {"users": [{"id": 1, "email": "[email protected]"}]}} 같은 응답을 받았습니다. data가 null일 수도 있고, users가 빈 배열일 수도 있습니다. 수동 파싱은 중첩 깊이마다 null 체크가 필요해 코드가 지저분해집니다.

시나리오 2: 설정 파일 버전 호환
config.jsontimeout 필드가 새 버전에서 추가됐는데, 구버전 설정 파일에는 없습니다. j["timeout"]으로 접근하면 null이 삽입되고, 나중에 get<int>() 호출 시 type_error가 발생합니다. 선택적 필드를 안전하게 처리하는 패턴이 필요합니다.

시나리오 3: 숫자 vs 문자열 혼동
API가 "age": 30(숫자)과 "age": "30"(문자열)을 혼용해서 보냅니다. get<int>()는 문자열에 실패하고, get<std::string>()은 숫자에 실패합니다. 타입 검증과 유연한 변환이 필요합니다.

시나리오 4: 대용량 JSON 메모리
수 MB 크기의 로그 파일을 한 번에 std::string으로 읽어 parse()하면 메모리가 두 배로 사용됩니다. 스트림 파싱으로 메모리 사용을 줄이고 싶습니다.

시나리오 5: 로그/이벤트 직렬화
분산 시스템에서 이벤트를 JSON 한 줄로 직렬화해 Kafka나 로그 파일에 씁니다. to_json으로 구조체를 자동 변환하고, dump()로 한 줄 출력이 필요합니다.

시나리오 6: NDJSON 스트리밍
로그 파일이 {"ts":1,"msg":"a"}\n{"ts":2,"msg":"b"}\n 형태의 NDJSON(Newline-Delimited JSON)입니다. 한 줄씩 파싱해 메모리를 절약하고 싶습니다.

시나리오 7: 타입 유연한 API
외부 API가 "count": 100(숫자) 또는 "count": "100"(문자열)을 상황에 따라 보냅니다. 두 형태 모두 처리하는 유연한 파서가 필요합니다.

nlohmann/json은 이런 문제들을 해결하는 헤더 전용(.cpp 없이 헤더만 include) C++ JSON 라이브러리입니다. STL과 비슷한 인터페이스([], contains, find, value), to_json/from_json으로 커스텀 타입 직렬화, 그리고 풍부한 예외 처리로 실무에서 널리 쓰입니다.

flowchart LR
  subgraph input["입력"]
    I1[문자열]
    I2[파일]
    I3[스트림]
  end
  subgraph nlohmann["nlohmann/json"]
    N1["json parse"]
    N2["객체 접근"]
    N3["j.dump()"]
    N4["타입 변환"]
  end
  subgraph output["출력"]
    O1[json 객체]
    O2[문자열]
    O3[커스텀 타입]
  end
  input --> N1
  N1 --> O1
  O1 --> N2
  O1 --> N4
  N4 --> O3
  O1 --> N3
  N3 --> O2

이 글을 읽으면:

  • nlohmann/json으로 JSON을 파싱·생성·접근할 수 있습니다.
  • 커스텀 타입 직렬화(to_json, from_json)를 구현할 수 있습니다.
  • JSON 검증과 에러 처리로 안전하게 사용할 수 있습니다.
  • 타입 불일치·누락 키 등 자주 나는 에러를 피할 수 있습니다.
  • 성능 비교프로덕션 패턴을 적용할 수 있습니다.

요구 환경: nlohmann/json은 헤더 전용이라 헤더만 포함하거나 vcpkg(vcpkg install nlohmann-json), Conan, FetchContent로 추가하면 됩니다. C++11 이상, 추가 빌드 설정 없이 사용 가능합니다.

목차

  1. 설치와 기본 사용
  2. 파싱·접근·직렬화 완전 가이드
  3. 커스텀 타입 직렬화 (to_json, from_json)
  4. JSON 검증과 에러 처리
  5. 자주 나는 에러와 해결법
  6. 베스트 프랙티스
  7. 성능 비교
  8. 프로덕션 패턴

개념을 잡는 비유

시간·파일·로그·JSON은 도구 상자의 자주 쓰는 렌치입니다. 표준·검증된 라이브러리로 한 가지 규칙을 정해 두면, 팀 전체가 같은 단위·같은 포맷으로 맞출 수 있습니다.


1. 설치와 기본 사용

헤더만 포함

#include <nlohmann/json.hpp>
using json = nlohmann::json;

단일 헤더(json.hpp)를 프로젝트에 복사하거나, 패키지 매니저로 설치한 뒤 include 경로만 맞추면 됩니다.

vcpkg로 설치

vcpkg install nlohmann-json

CMake에서 find_package(nlohmann_json CONFIG REQUIRED)target_link_libraries(your_target PRIVATE nlohmann_json::nlohmann_json)로 연동합니다.

FetchContent (CMake)

include(FetchContent)
FetchContent_Declare(
  json
  GIT_REPOSITORY https://github.com/nlohmann/json.git
  GIT_TAG v3.11.2
)
FetchContent_MakeAvailable(json)
target_link_libraries(your_target PRIVATE nlohmann_json::nlohmann_json)

첫 예제

json j = {{“name”, “Alice”}, {“age”, 30}}는 중괄호 초기화로 JSON 객체를 만듭니다. j[“name”], j[“age”]로 키에 접근하면 해당 값이 나오고, 문자열은 std::string으로, 숫자는 int 등으로 자동 변환됩니다. 키가 없을 때 j[“key”]null을 넣어 버리므로, 존재 여부를 확인하려면 j.contains(“key”) 또는 j.find(“key”) != j.end()를 먼저 쓰는 것이 안전합니다. API 응답 파싱 시 이 패턴으로 필드가 있는지 확인한 뒤 읽으면 런타임 오류를 줄일 수 있습니다.

// 복사해 붙여넣은 뒤: g++ -std=c++17 -o json_basic json_basic.cpp -I<nlohmann/json 경로> && ./json_basic
// (vcpkg/Conan/FetchContent로 nlohmann-json 설치 후 -I 경로만 맞추면 됨)
#include <nlohmann/json.hpp>
#include <iostream>

using json = nlohmann::json;

int main() {
    json j = {{"name", "Alice"}, {"age", 30}};
    std::cout << j["name"] << "\n";  // "Alice"
    std::cout << j["age"] << "\n";   // 30
    return 0;
}

실행 결과: "Alice"30 이 각각 한 줄씩 출력됩니다.


2. 파싱·접근·직렬화 완전 가이드

문자열 파싱

R”(…)”는 raw string 리터럴이라 이스케이프 없이 따옴표를 그대로 쓸 수 있습니다. json::parse(str)는 문자열을 파싱해 json 객체로 만들고, 파싱 실패 시 json::parse_error 예외를 던집니다. 예외 없이 처리하려면 json::parse(str, nullptr, false)nullptr 반환을 받을 수 있습니다.

#include <nlohmann/json.hpp>
#include <string>
#include <iostream>

using json = nlohmann::json;

int main() {
    // 1. 기본 문자열 파싱
    std::string str = R"({"key": "value", "num": 42})";
    json j = json::parse(str);

    // get<T>로 명시적 타입 변환
    std::string key = j["key"].get<std::string>();
    int num = j["num"].get<int>();
    std::cout << key << ", " << num << "\n";  // value, 42

    // 2. 중첩 객체 파싱
    std::string nested = R"({
        "user": {"name": "Alice", "age": 30},
        "tags": ["admin", "user"]
    })";
    json j2 = json::parse(nested);
    std::string name = j2["user"]["name"].get<std::string>();
    int age = j2["user"]["age"].get<int>();
    std::cout << name << ", " << age << "\n";  // Alice, 30

    return 0;
}

실행 결과: value, 42Alice, 30이 각각 출력됩니다.

파일 파싱

json::parsestd::istream도 받을 수 있어서, std::ifstream으로 연 파일을 넘기면 파일 내용 전체를 JSON으로 파싱합니다. 문자열로 먼저 읽지 않아 메모리 효율적입니다.

#include <nlohmann/json.hpp>
#include <fstream>
#include <stdexcept>
#include <string>

using json = nlohmann::json;

json load_config(const std::string& path) {
    std::ifstream f(path);
    if (!f) {
        throw std::runtime_error("Cannot open file: " + path);
    }
    try {
        return json::parse(f);
    } catch (const json::parse_error& e) {
        throw std::runtime_error(std::string("JSON parse error: ") + e.what());
    }
}

// 사용 예: config.json
// {
//   "port": 8080,
//   "host": "0.0.0.0"
// }
// json j = load_config("config.json");
// int port = j.value("port", 8080);

config.json 예시:

{
  "port": 8080,
  "host": "0.0.0.0",
  "timeout": 30
}

dump: JSON을 문자열로 직렬화

j.dump()는 한 줄로 압축된 JSON 문자열을 반환하고, j.dump(2)는 들여쓰기 2칸으로 예쁘게 출력합니다. API 요청 본문이나 로그에 쓸 때는 dump()로 직렬화합니다.

#include <nlohmann/json.hpp>
#include <iostream>

using json = nlohmann::json;

int main() {
    json j = {{"name", "Bob"}, {"scores", {10, 20, 30}}};

    // 한 줄 압축 (API 요청, 로그에 적합)
    std::string compact = j.dump();
    std::cout << compact << "\n";  // {"name":"Bob","scores":[10,20,30]}

    // 들여쓰기 2칸 (디버깅, 설정 파일 저장에 적합)
    std::string pretty = j.dump(2);
    std::cout << pretty << "\n";

    return 0;
}

dump 옵션: dump(indent)indent=-1이면 압축, 2면 들여쓰기 2칸. dump(indent, indent_char, ensure_ascii)로 한글 등 비ASCII 문자 이스케이프 여부를 제어할 수 있습니다.

안전한 접근 패턴

방법키 없을 때null일 때
j[“key”]null 삽입 후 반환 (위험)그대로 반환
j.contains(“key”)falsetrue
j.value(“key”, default)default 반환default 반환
j.find(“key”)end()iterator 반환
// ❌ 위험: 키가 없으면 null이 삽입됨
auto v = j["optional_key"];

// ✅ 안전: contains로 먼저 확인
if (j.contains("optional_key")) {
    auto v = j["optional_key"];
}

// ✅ 안전: value로 기본값 지정
auto v = j.value("optional_key", "default");
int age = j.value("age", 0);

배열 순회

j[“items”]가 배열이면 for (auto& item : j[“items”])로 각 요소를 순회할 수 있습니다.

json j = json::parse(R"({"data": [{"id": 1, "name": "A"}, {"id": 2, "name": "B"}]})");

for (auto& item : j["data"]) {
    int id = item["id"].get<int>();
    std::string name = item["name"].get<std::string>();
    std::cout << id << ": " << name << "\n";
}

객체 생성과 수정

json j;
j["name"] = "Bob";
j["scores"] = {10, 20, 30};
j["nested"] = {{"a", 1}, {"b", 2}};

// 배열에 요소 추가
j["tags"].push_back("c++");
j["tags"].push_back("json");

// 중첩 접근
j["nested"]["c"] = 3;

3. 커스텀 타입 직렬화 (to_json, from_json)

기본 패턴

nlohmann::adl_serializer를 사용해 to_jsonfrom_json을 정의하면, j.get<User>()json(obj)로 자동 변환됩니다.

#include <nlohmann/json.hpp>
#include <string>

using json = nlohmann::json;

struct User {
    std::string name;
    int age;
    std::vector<std::string> tags;
};

// JSON → User (역직렬화)
void from_json(const json& j, User& u) {
    j.at("name").get_to(u.name);
    j.at("age").get_to(u.age);
    if (j.contains("tags")) {
        j.at("tags").get_to(u.tags);
    }
}

// User → JSON (직렬화)
void to_json(json& j, const User& u) {
    j = json{
        {"name", u.name},
        {"age", u.age},
        {"tags", u.tags}
    };
}

int main() {
    std::string str = R"({"name": "Alice", "age": 30, "tags": ["admin", "user"]})";
    json j = json::parse(str);

    User u = j.get<User>();
    std::cout << u.name << ", " << u.age << "\n";

    json j2 = u;  // to_json 자동 호출
    std::cout << j2.dump(2) << "\n";

    return 0;
}

at() vs []의 차이

  • j.at(“key”): 키가 없으면 out_of_range 예외 발생. 검증이 필요할 때 사용.
  • j[“key”]: 키가 없으면 null 삽입 후 반환. 주의해서 사용.

선택적 필드 처리

struct Config {
    int port = 8080;           // 기본값
    std::string host = "localhost";
    std::optional<int> timeout;  // 선택적
};

void from_json(const json& j, Config& c) {
    if (j.contains("port")) c.port = j["port"].get<int>();
    if (j.contains("host")) c.host = j["host"].get<std::string>();
    if (j.contains("timeout")) c.timeout = j["timeout"].get<int>();
}

void to_json(json& j, const Config& c) {
    j = {{"port", c.port}, {"host", c.host}};
    if (c.timeout) j["timeout"] = *c.timeout;
}

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE (C++17)

매크로로 반복 코드를 줄일 수 있습니다.

struct Point {
    double x;
    double y;
};

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Point, x, y)

// 사용
Point p{1.0, 2.0};
json j = p;
Point p2 = j.get<Point>();

enum과 중첩 구조체 직렬화

enum을 문자열로 직렬화하고, 중첩 구조체를 재귀적으로 처리하는 예제입니다.

#include <nlohmann/json.hpp>
#include <string>
#include <vector>

using json = nlohmann::json;

enum class UserRole { Admin, User, Guest };

// enum → 문자열
void to_json(json& j, UserRole r) {
    switch (r) {
        case UserRole::Admin: j = "admin"; break;
        case UserRole::User:  j = "user";  break;
        case UserRole::Guest: j = "guest";  break;
    }
}

void from_json(const json& j, UserRole& r) {
    std::string s = j.get<std::string>();
    if (s == "admin") r = UserRole::Admin;
    else if (s == "user") r = UserRole::User;
    else r = UserRole::Guest;
}

struct Address {
    std::string city;
    std::string zip;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Address, city, zip)

struct UserWithAddress {
    std::string name;
    UserRole role;
    Address address;
};

void to_json(json& j, const UserWithAddress& u) {
    j = {{"name", u.name}, {"role", u.role}, {"address", u.address}};
}

void from_json(const json& j, UserWithAddress& u) {
    j.at("name").get_to(u.name);
    j.at("role").get_to(u.role);
    j.at("address").get_to(u.address);
}

// 사용 예
// json j = UserWithAddress{"Alice", UserRole::Admin, {"Seoul", "12345"}};
// std::cout << j.dump(2);

std::optional과 선택적 필드

C++17 std::optional로 선택적 필드를 안전하게 처리합니다.

#include <nlohmann/json.hpp>
#include <optional>
#include <string>

using json = nlohmann::json;

struct Product {
    std::string id;
    std::string name;
    std::optional<double> price;  // 없을 수 있음
};

void from_json(const json& j, Product& p) {
    j.at("id").get_to(p.id);
    j.at("name").get_to(p.name);
    if (j.contains("price") && !j["price"].is_null()) {
        p.price = j["price"].get<double>();
    }
}

void to_json(json& j, const Product& p) {
    j = {{"id", p.id}, {"name", p.name}};
    if (p.price) j["price"] = *p.price;
}

4. JSON 검증과 에러 처리

파싱 예외

json::parse는 문법 오류 시 json::parse_error를 던집니다. e.what()e.byte로 오류 위치를 확인할 수 있습니다.

#include <nlohmann/json.hpp>
#include <iostream>

using json = nlohmann::json;

json safe_parse(const std::string& str) {
    try {
        return json::parse(str);
    } catch (const json::parse_error& e) {
        std::cerr << "JSON 파싱 오류: " << e.what() << "\n";
        std::cerr << "바이트 위치: " << e.byte << "\n";
        return json::object();  // 빈 객체로 폴백
    }
}

타입 예외

get<T>에서 타입이 맞지 않으면 json::type_error가 발생합니다.

try {
    int x = j["name"].get<int>();  // "name"이 문자열이면 type_error
} catch (const json::type_error& e) {
    std::cerr << "타입 오류: " << e.what() << "\n";
}

is_* 메서드로 사전 검증

타입을 먼저 확인한 뒤 get<T>()를 호출하면 type_error를 방지할 수 있습니다.

if (j["age"].is_number_integer()) {
    int age = j["age"].get<int>();
} else if (j["age"].is_string()) {
    int age = std::stoi(j["age"].get<std::string>());
}
if (j["data"].is_array()) {
    for (auto& item : j["data"]) {
        if (item.is_object() && item.contains("id")) {
            int id = item["id"].get<int>();
        }
    }
}
// 지원: is_null, is_boolean, is_number, is_number_integer,
//       is_number_float, is_string, is_array, is_object

JSON Schema 검증 (선택)

nlohmann/json 자체에는 스키마 검증이 없습니다. nlohmann/json-schema 또는 valijson 같은 별도 라이브러리로 스키마 검증을 할 수 있습니다.


5. 자주 나는 에러와 해결법

에러 1: type_error — 타입 불일치

증상: "type must be number, but is string" 같은 메시지.

원인: JSON 필드가 문자열인데 get<int>()로 읽거나, 숫자인데 get<std::string>()으로 읽음.

// ❌ 잘못된 예: "age"가 "30" (문자열)인 경우
int age = j["age"].get<int>();  // type_error!

// ✅ 해결 1: is_* 로 검증 후 변환
if (j["age"].is_number_integer()) {
    int age = j["age"].get<int>();
} else if (j["age"].is_string()) {
    int age = std::stoi(j["age"].get<std::string>());
}

// ✅ 해결 2: value + 기본값
int age = j.value("age", 0);

에러 2: out_of_range — 누락된 키

증상: j.at("required_key") 호출 시 키가 없으면 out_of_range 예외.

원인: API 응답에 필드가 없거나, 설정 파일에 키가 누락됨.

// ❌ at()은 키 없으면 예외
auto name = j.at("name").get<std::string>();

// ✅ contains로 먼저 확인
if (j.contains("name")) {
    auto name = j["name"].get<std::string>();
}

// ✅ value로 기본값
auto name = j.value("name", std::string("unknown"));

에러 3: j[“key”]가 null을 삽입함

증상: 읽기 전용인데 j["nonexistent"]를 호출하면 객체에 null이 추가됨.

원인: operator[]는 키가 없으면 null을 삽입한 뒤 반환합니다.

// ❌ 읽기만 할 때도 객체가 수정됨
if (j["optional"] != nullptr) { ... }  // "optional" 키가 생김!

// ✅ contains 또는 find 사용
if (j.contains("optional")) {
    auto v = j["optional"];
}
// 또는
auto it = j.find("optional");
if (it != j.end()) {
    auto v = *it;
}

에러 4: parse_error — 잘못된 JSON 문법

증상: "parse error at line 1, column 10" 같은 메시지.

원인: trailing comma, 따옴표 누락, 인코딩 문제 등.

// ❌ 잘못된 JSON
std::string bad = R"({"name": "Alice",})";  // trailing comma

// ✅ try/catch로 처리
try {
    json j = json::parse(bad);
} catch (const json::parse_error& e) {
    std::cerr << "파싱 실패: " << e.what() << "\n";
}

에러 5: 순환 참조

증상: to_json에서 무한 재귀 또는 스택 오버플로우.

원인: 자기 자신을 참조하는 구조체를 직렬화할 때.

struct Node {
    std::string value;
    Node* parent;  // 순환 참조 가능
};

// ✅ parent는 직렬화에서 제외하거나, ID로 대체
void to_json(json& j, const Node& n) {
    j = {{"value", n.value}};
    // parent 제외
}

에러 6: 부동소수점 정밀도 손실

증상: 3.14159265358979가 파싱 후 3.14159로 잘리거나, 금융 계산에서 오차 발생.

원인: JSON은 IEEE 754 double을 사용. floatget<float>()하면 정밀도가 줄어듭니다.

// ❌ float 사용 시 정밀도 손실
float price = j["price"].get<float>();

// ✅ 금융/정밀 계산은 double 또는 decimal 라이브러리 사용
double price = j["price"].get<double>();

// ✅ 매우 큰 정수는 문자열로 처리 (JavaScript Number 한계)
std::string big_id = j["id"].get<std::string>();

에러 7: UTF-8 BOM 및 인코딩

증상: 파일 파싱 시 "parse error at line 1, column 1" 또는 첫 문자 깨짐.

원인: Windows에서 저장한 JSON이 UTF-8 BOM(EF BB BF)으로 시작. nlohmann/json 3.11+는 BOM을 자동 처리하지만, 구버전이나 스트림에서는 수동 제거 필요.

// ✅ BOM 제거 후 파싱 (구버전 호환)
std::string read_json_file(const std::string& path) {
    std::ifstream f(path, std::ios::binary);
    std::string content((std::istreambuf_iterator<char>(f)),
                         std::istreambuf_iterator<char>());
    if (content.size() >= 3 &&
        static_cast<unsigned char>(content[0]) == 0xEF &&
        static_cast<unsigned char>(content[1]) == 0xBB &&
        static_cast<unsigned char>(content[2]) == 0xBF) {
        content = content.substr(3);
    }
    return content;
}

에러 8: 빈 문자열/배열 타입 혼동

증상: j["items"][]일 때 get<std::vector<int>>()는 성공하지만, j["items"][0] 접근 시 인덱스 오류.

원인: 빈 배열은 유효한 JSON. 순회 전 size() 또는 empty() 확인 필요.

// ❌ 빈 배열일 때 item["id"] 접근 시 문제
for (auto& item : j["data"]) {
    int id = item["id"].get<int>();  // 빈 배열이면 순회 안 함 (OK)
}
// 하지만 j["data"][0] 직접 접근 시
// int x = j["data"][0]["id"];  // data가 []면 type_error!

// ✅ size() 확인 후 접근
if (!j["data"].empty()) {
    auto first = j["data"][0];
}

6. 베스트 프랙티스

6.1 필수 필드 vs 선택 필드 구분

필수 필드는 at()으로 검증하고, 선택 필드는 contains() + value()로 처리합니다.

#include <nlohmann/json.hpp>
#include <optional>

using json = nlohmann::json;

struct ApiResponse {
    int code;                    // 필수
    std::string message;         // 필수
    std::optional<json> data;    // 선택 (구조가 가변적일 때)
};

void from_json(const json& j, ApiResponse& r) {
    r.code = j.at("code").get<int>();
    r.message = j.at("message").get<std::string>();
    if (j.contains("data") && !j["data"].is_null()) {
        r.data = j["data"];
    }
}

6.2 파싱 래퍼 일원화

프로젝트 전체에서 json::parse를 직접 호출하지 않고, 로깅·폴백을 포함한 래퍼를 사용합니다.

#include <nlohmann/json.hpp>
#include <optional>
#include <functional>

using json = nlohmann::json;

std::optional<json> parse_json_safe(const std::string& input,
    std::function<void(const std::string&)> on_error = nullptr) {
    try {
        return json::parse(input);
    } catch (const json::parse_error& e) {
        if (on_error) on_error(e.what());
        return std::nullopt;
    }
}

// 사용
auto j = parse_json_safe(api_response,  {
    std::cerr << "JSON 파싱 실패: " << msg << "\n";
});
if (j) { /* 정상 처리 */ }

6.3 네임스페이스와 ADL

to_json/from_jsonADL(Argument-Dependent Lookup)으로 찾습니다. 구조체와 같은 네임스페이스에 두거나, nlohmann 네임스페이스에 특수화합니다.

namespace myapp {
struct User { std::string name; int age; };

void to_json(json& j, const User& u) {
    j = {{"name", u.name}, {"age", u.age}};
}
void from_json(const json& j, User& u) {
    j.at("name").get_to(u.name);
    j.at("age").get_to(u.age);
}
}  // namespace myapp

6.4 불변성 유지

읽기 전용 접근 시 j["key"] 대신 j.contains()를 먼저 확인해 원본 객체에 null을 삽입하지 않습니다.

6.5 dump 옵션 프로젝트별 통일

API 요청은 dump()(압축), 로그/디버깅은 dump(2)로 통일해 가독성과 일관성을 유지합니다.


7. 성능 비교

nlohmann/json vs 다른 라이브러리 (개념적 비교)

라이브러리파싱 속도메모리헤더 전용사용 편의
nlohmann/json보통보통매우 좋음
RapidJSON빠름적음보통
simdjson매우 빠름적음보통
jsoncpp느림많음좋음

nlohmann/json은 “편의성과 타입 안전성”에 초점을 맞춘 라이브러리입니다. 초당 수만 건 수준의 API 응답 파싱에는 충분하고, 초당 수십만 건 이상이 필요하면 RapidJSON이나 simdjson을 고려할 수 있습니다.

파싱 최적화 팁

// 1. 큰 파일은 스트림으로 파싱 (메모리 절약)
std::ifstream f("large.json");
json j = json::parse(f);

// 2. 반복 파싱 시 문자열 재사용
std::string buffer;
buffer.reserve(4096);
// ... buffer에 데이터 채운 뒤
json j = json::parse(buffer);

// 3. dump 결과 캐싱
std::string cached = j.dump();
// 여러 번 사용할 때 한 번만 dump

컴파일 시간

헤더 전용이라 include하는 순간 컴파일 시간이 늘어납니다. nlohmann/json_fwd.hpp를 사용하면 전방 선언만 하고, 실제 사용하는 .cpp에서만 json.hpp를 include해 컴파일 시간을 줄일 수 있습니다.

// header.h
#include <nlohmann/json_fwd.hpp>
void process(const nlohmann::json& j);

// impl.cpp
#include <nlohmann/json.hpp>  // 여기서만 전체 정의

SAX/이벤트 기반 파싱 (대용량)

수십 MB 이상의 JSON에서 특정 키만 추출할 때는 json::sax 파서를 사용해 DOM 전체를 만들지 않고 스트리밍으로 처리할 수 있습니다.

#include <nlohmann/json.hpp>
#include <iostream>

using json = nlohmann::json;

struct KeyExtractor : json::json_sax_t {
    std::string target_key;
    std::vector<std::string> values;

    bool string(string_t& val) override {
        if (current_key == target_key) {
            values.push_back(val);
        }
        return true;
    }
    bool key(string_t& val) override {
        current_key = val;
        return true;
    }
    std::string current_key;
};

// 사용: {"users":[{"name":"A"},{"name":"B"}]} 에서 "name" 값만 추출

벤치마크 참고 수치 (개념적)

작업nlohmann/json (대략)RapidJSON (대략)
1MB JSON 파싱~20ms~8ms
1MB dump~15ms~10ms
메모리 오버헤드파싱 크기의 2~3배1~1.5배

실제 수치는 하드웨어·컴파일러·JSON 구조에 따라 달라지므로, 프로젝트에서 직접 측정하는 것이 좋습니다.


8. 프로덕션 패턴

API 응답 처리 흐름

sequenceDiagram
    participant Client as C++ 클라이언트
    participant API as REST API
    participant JSON as nlohmann/json

    Client->>API: HTTP GET /users
    API->>Client: {"data":[{"id":1,"name":"Alice"}]}
    Client->>JSON: json::parse(response_body)
    JSON->>Client: json 객체
    Client->>JSON: res["data"].get<vector<User>>()
    JSON->>Client: vector<User>

API 응답 처리

#include <nlohmann/json.hpp>
#include <string>
#include <vector>

using json = nlohmann::json;

struct ApiItem {
    int id;
    std::string name;
};

void from_json(const json& j, ApiItem& item) {
    j.at("id").get_to(item.id);
    j.at("name").get_to(item.name);
}

std::vector<ApiItem> parse_api_response(const std::string& response_body) {
    std::vector<ApiItem> result;

    try {
        json res = json::parse(response_body);

        if (!res.contains("data") || !res["data"].is_array()) {
            return result;
        }

        for (auto& item : res["data"]) {
            result.push_back(item.get<ApiItem>());
        }
    } catch (const json::exception& e) {
        // 로깅 후 빈 결과 반환
        return result;
    }

    return result;
}

설정 파일 로드

#include <nlohmann/json.hpp>
#include <fstream>
#include <optional>

using json = nlohmann::json;

struct AppConfig {
    int port = 8080;
    std::string host = "0.0.0.0";
    std::optional<std::string> log_level;
};

void from_json(const json& j, AppConfig& c) {
    if (j.contains("port")) c.port = j["port"].get<int>();
    if (j.contains("host")) c.host = j["host"].get<std::string>();
    if (j.contains("log_level")) c.log_level = j["log_level"].get<std::string>();
}

AppConfig load_config(const std::string& path) {
    std::ifstream f(path);
    if (!f) {
        return AppConfig{};  // 기본 설정 반환
    }

    try {
        return json::parse(f).get<AppConfig>();
    } catch (const json::exception& e) {
        return AppConfig{};
    }
}

요청 본문 생성

json create_request_body(const std::string& action, const std::vector<int>& ids) {
    return {
        {"action", action},
        {"ids", ids},
        {"timestamp", std::time(nullptr)}
    };
}

// HTTP 클라이언트에 전달
std::string body = create_request_body("delete", {1, 2, 3}).dump();

로그 직렬화

struct LogEntry {
    std::string level;
    std::string message;
    std::time_t timestamp;
};

void to_json(json& j, const LogEntry& e) {
    j = {
        {"level", e.level},
        {"message", e.message},
        {"timestamp", e.timestamp}
    };
}

// 로그를 JSON 한 줄로 출력
LogEntry entry{"INFO", "User logged in", std::time(nullptr)};
std::cout << json(entry).dump() << "\n";

에러 복구 가능한 파싱 래퍼

외부 API나 사용자 입력은 항상 잘못된 JSON일 수 있으므로, 파싱 실패 시 로깅하고 기본값을 반환하는 래퍼를 두는 것이 좋습니다.

#include <nlohmann/json.hpp>
#include <optional>
#include <iostream>

using json = nlohmann::json;

struct ParseResult {
    json data;
    bool ok;
    std::string error_message;
};

ParseResult safe_parse_with_logging(const std::string& input) {
    try {
        return {json::parse(input), true, ""};
    } catch (const json::parse_error& e) {
        std::cerr << "[JSON] 파싱 실패: " << e.what()
                  << " (byte " << e.byte << ")\n";
        return {json::object(), false, e.what()};
    }
}

// 사용
auto result = safe_parse_with_logging(api_response);
if (result.ok) {
    // 정상 처리
} else {
    // 폴백 또는 재시도
}

설정 파일 환경별 오버라이드

환경 변수로 JSON 설정을 오버라이드하는 패턴입니다.

#include <nlohmann/json.hpp>
#include <cstdlib>
#include <fstream>
#include <string>

using json = nlohmann::json;

json load_config_with_env_override(const std::string& path) {
    std::ifstream f(path);
    json j = f ? json::parse(f) : json::object();

    // 환경 변수로 오버라이드
    if (const char* port = std::getenv("APP_PORT")) {
        j["port"] = std::stoi(port);
    }
    if (const char* host = std::getenv("APP_HOST")) {
        j["host"] = host;
    }
    return j;
}

NDJSON(Newline-Delimited JSON) 스트리밍

로그·이벤트 스트림을 한 줄씩 파싱해 메모리 사용을 최소화합니다.

void process_ndjson(const std::string& path, auto on_line) {
    std::ifstream f(path);
    std::string line;
    while (std::getline(f, line)) {
        if (line.empty()) continue;
        try { on_line(json::parse(line)); }
        catch (const json::parse_error&) { /* 스킵 */ }
    }
}

숫자/문자열 혼용 필드 처리

API가 "count": 100 또는 "count": "100"을 보낼 때 모두 처리하는 헬퍼입니다.

int get_int_flexible(const json& j, const std::string& key, int d = 0) {
    if (!j.contains(key)) return d;
    const auto& v = j[key];
    if (v.is_number_integer()) return v.get<int>();
    if (v.is_string()) return std::stoi(v.get<std::string>());
    return d;
}

프로덕션 체크리스트

  • 파싱: try/catch로 parse_error 처리
  • 접근: contains 또는 value로 누락 키 방어
  • 타입: is_* 검증 또는 get<T> 예외 처리
  • 커스텀 타입: to_json/from_json로 도메인 객체 매핑
  • 대용량: 스트림 파싱, dump 캐싱
  • 컴파일 시간: json_fwd.hpp 활용
  • 로깅: 파싱 실패 시 에러 메시지와 위치 기록
  • 환경 연동: 환경 변수로 설정 오버라이드 지원

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • C++ Boost 라이브러리 | Asio·Filesystem·Regex·설치부터 프로덕션까지 완벽 가이드
  • C++ 로깅 라이브러리 (spdlog) | 빠른 로깅과 다중 싱크 [#27-3]
  • C++ 소켓 프로그래밍 완벽 가이드 | TCP/UDP·소켓 옵션·논블로킹·에러 처리 [#28-1]

이 글에서 다루는 키워드 (관련 검색어)

C++ JSON, nlohmann json, JSON 파싱, JSON 직렬화, to_json from_json, API 응답 파싱, 설정 파일 JSON 등으로 검색하시면 이 글이 도움이 됩니다.


정리

항목내용
파싱json::parse(string/stream), parse_error 처리
접근j[“key”], j.value(“key”, default), contains, find
직렬화j.dump(), j.dump(2)
커스텀 타입to_json, from_json, NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE
검증is_*, at(), try/catch
자주 나는 에러type_error, out_of_range, operator[] null 삽입, 부동소수점, 인코딩
베스트 프랙티스contains/value 사용, 파싱 try/catch, 스트림 파싱, json_fwd.hpp
성능스트림 파싱, dump 캐싱
프로덕션API 응답, 설정 파일, 로그 직렬화, 에러 복구 래퍼, 환경 변수 오버라이드

자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. REST API 응답 파싱, 설정 파일 로드, 로그 직렬화, 마이크로서비스 간 데이터 교환 등 JSON을 다루는 모든 C++ 프로젝트에서 활용합니다. nlohmann/json은 헤더 전용이라 빌드 연동이 쉽고, STL 친화적인 API로 학습 곡선이 낮습니다.

Q. RapidJSON이나 simdjson과 비교하면?

A. nlohmann/json은 편의성과 타입 안전성에 강점이 있고, RapidJSON/simdjson은 파싱 속도와 메모리 효율에 강점이 있습니다. 초당 수만 건 수준이면 nlohmann/json으로 충분하고, 고성능이 필요하면 벤치마크 후 선택하세요.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. nlohmann/json 공식 저장소, cppreference, JSON Schema 검증 라이브러리를 참고하세요.



한 줄 요약: nlohmann/json으로 JSON 파싱·생성을 타입 안전하게 할 수 있습니다. to_json/from_json으로 커스텀 타입 직렬화, contains/value로 안전한 접근, 프로덕션 패턴까지 적용해 보세요.

이전 글: C++ 실전 가이드 #27-1: Boost

다음 글: [C++ 실전 가이드 #27-3] 로깅 라이브러리 (spdlog): 빠른 로깅과 다중 싱크


관련 글

  • C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴