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

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

이 글의 핵심

C++ JSON 처리: nlohmann/json으로 파싱과 생성하기 [#27-2]. JSON 파싱이 복잡해요·실무에서 겪은 문제.

들어가며: 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": "a@b.com"}]}} 같은 응답을 받았습니다. 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으로 커스텀 타입 직렬화, 그리고 풍부한 예외 처리로 실무에서 널리 쓰입니다. 다음은 mermaid를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 실행 예제
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 이상, 추가 빌드 설정 없이 사용 가능합니다. 실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

목차

  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)

아래 코드는 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 응답 파싱 시 이 패턴으로 필드가 있는지 확인한 뒤 읽으면 런타임 오류를 줄일 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 복사해 붙여넣은 뒤: 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 반환을 받을 수 있습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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으로 파싱합니다. 문자열로 먼저 읽지 않아 메모리 효율적입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 예시: 아래 코드는 json를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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

dump: JSON을 문자열로 직렬화

j.dump()는 한 줄로 압축된 JSON 문자열을 반환하고, j.dump(2)는 들여쓰기 2칸으로 예쁘게 출력합니다. API 요청 본문이나 로그에 쓸 때는 dump()로 직렬화합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 반환
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ 위험: 키가 없으면 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])로 각 요소를 순회할 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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";
}

객체 생성과 수정

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)로 자동 변환됩니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 삽입 후 반환. 주의해서 사용.

선택적 필드 처리

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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)

매크로로 반복 코드를 줄일 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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을 문자열로 직렬화하고, 중첩 구조체를 재귀적으로 처리하는 예제입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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로 선택적 필드를 안전하게 처리합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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로 오류 위치를 확인할 수 있습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#include <nlohmann/json.hpp>
#include <iostream>
// 필요한 모듈 import
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가 발생합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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를 방지할 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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>()으로 읽음. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 잘못된 예: "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 응답에 필드가 없거나, 설정 파일에 키가 누락됨. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 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을 삽입한 뒤 반환합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 읽기만 할 때도 객체가 수정됨
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, 따옴표 누락, 인코딩 문제 등. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 잘못된 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에서 무한 재귀 또는 스택 오버플로우. 원인: 자기 자신을 참조하는 구조체를 직렬화할 때. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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>()하면 정밀도가 줄어듭니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

// ❌ 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을 자동 처리하지만, 구버전이나 스트림에서는 수동 제거 필요. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ✅ 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() 확인 필요. 아래 코드는 cpp를 사용한 구현 예제입니다. 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// ❌ 빈 배열일 때 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()로 처리합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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를 직접 호출하지 않고, 로깅·폴백을 포함한 래퍼를 사용합니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 네임스페이스에 특수화합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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을 고려할 수 있습니다.

파싱 최적화 팁

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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해 컴파일 시간을 줄일 수 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 코드를 직접 실행해보면서 동작을 확인해보세요.

// 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 전체를 만들지 않고 스트리밍으로 처리할 수 있습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 응답 처리 흐름

아래 코드는 mermaid를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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>()
    JSON->>Client: vector

API 응답 처리

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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;
}

설정 파일 로드

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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{};
    }
}

요청 본문 생성

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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();

로그 직렬화

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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일 수 있으므로, 파싱 실패 시 로깅하고 기본값을 반환하는 래퍼를 두는 것이 좋습니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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 설정을 오버라이드하는 패턴입니다. 다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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) 스트리밍

로그·이벤트 스트림을 한 줄씩 파싱해 메모리 사용을 최소화합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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"을 보낼 때 모두 처리하는 헬퍼입니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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++ 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): 빠른 로깅과 다중 싱크

관련 글

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3