C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴
이 글의 핵심
REST API 응답·설정 파일 파싱 시 타입 에러·null 접근·메모리 폭발 문제를 해결합니다. nlohmann/json, RapidJSON, 커스텀 타입 직렬화, 자주 발생하는 에러, 베스트 프랙티스, 프로덕션 패턴까지 실전 코드로 다룹니다.
들어가며: “API 응답 파싱하다가 크래시가 나요”
실제 겪는 문제 시나리오
// ❌ 문제: HTTP 클라이언트로 API 응답을 받았는데...
// - {"users": [{"id": 1, "name": "Alice"}]} 를 어떻게 구조체로?
// - "age"가 30(숫자)인데 "30"(문자열)으로 오면?
// - 키가 없는데 j["optional"] 접근 → null 삽입 → 나중에 get<int>() 크래시
// - 10MB JSON을 한 번에 메모리에 올리면 OOM
실제 프로덕션에서 겪는 문제들:
- 타입 불일치: API가
"age": 30(숫자)과"age": "30"(문자열)을 혼용 →get<int>()실패 - null/누락 키 접근:
j["optional"]이 없으면 null이 삽입되고, 이후get<T>()에서type_error발생 - 파싱 실패: 잘못된 JSON(쉼표 누락, 따옴표 미닫힘) →
parse_error예외 - 메모리 폭발: 대용량 JSON을
std::string으로 읽어parse()하면 원본+파싱 결과로 메모리 2배 - 커스텀 타입 변환:
User,Config같은 구조체 ↔ JSON 수동 변환은 번거롭고 실수하기 쉬움
해결책:
- 적절한 라이브러리 선택: nlohmann/json(편의성), RapidJSON(성능)
- 안전한 접근 패턴:
contains,value,at으로 null/누락 방지 - 커스텀 직렬화:
to_json/from_json으로 타입 안전성 확보 - 에러 분류: 파싱 에러 vs 타입 에러 vs 비즈니스 검증 분리
- 스트림 파싱: 대용량 시 SAX 스타일로 메모리 절약
목표:
- nlohmann/json과 RapidJSON 완전 파싱 예제
- 커스텀 타입 직렬화 (구조체, enum, optional)
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스와 프로덕션 패턴
- 성능 비교와 선택 가이드
이 글을 읽으면:
- REST API 응답을 안전하게 파싱할 수 있습니다.
- 설정 파일·이벤트 로그를 JSON으로 직렬화/역직렬화할 수 있습니다.
- 타입 에러·null 접근 등 자주 나는 버그를 피할 수 있습니다.
- 프로덕션 환경에 맞는 패턴을 적용할 수 있습니다.
목차
- 문제 시나리오와 해결 방향
- 라이브러리 비교: nlohmann vs RapidJSON
- nlohmann/json 완전 파싱 예제
- RapidJSON 완전 파싱 예제
- 커스텀 타입 직렬화
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 성능 비교와 선택 가이드
- 프로덕션 패턴
1. 문제 시나리오와 해결 방향
1.1 전형적인 실패 패턴
sequenceDiagram
participant App as 애플리케이션
participant HTTP as HTTP 클라이언트
participant API as 외부 API
App->>HTTP: GET /api/users
HTTP->>API: 요청
API-->>HTTP: {"data": [{"id": 1, "name": "Alice"}]}
HTTP-->>App: body (문자열)
Note over App: 파싱 필요
App->>App: json::parse(body)
Note over App: j["data"][0]["age"] 접근<br/>age 키 없음 → null
App->>App: .get<int>() → type_error!
문제 요약:
- HTTP 응답 본문은 문자열 → 구조화된 데이터로 변환 필요
- 키 누락·null·타입 불일치 시 런타임 크래시
- 수동 파싱(strstr, 정규식)은 이스케이프·중첩 처리 지옥
1.2 추가 문제 시나리오
시나리오 1: 외부 API 응답 버전 차이
API v1은 {"user": {"name": "Alice"}}, v2는 {"user": null}을 반환합니다. j["user"]["name"] 접근 시 v2에서 null 참조로 크래시합니다.
시나리오 2: 설정 파일 마이그레이션
config.json에 timeout 필드가 새 버전에서 추가됐는데, 구버전 설정에는 없습니다. j["timeout"].get<int>() 호출 시 type_error가 발생합니다.
시나리오 3: 숫자 vs 문자열 혼용
일부 API는 "count": 100, 다른 엔드포인트는 "count": "100"을 보냅니다. 타입 검증 없이 get<int>()만 쓰면 파싱 실패가 발생합니다.
시나리오 4: 대용량 로그 파일
수 MB 크기의 JSON 로그를 std::string으로 읽어 parse()하면 메모리가 두 배로 사용됩니다. 스트림 파싱 또는 SAX API로 메모리를 절약해야 합니다.
시나리오 5: 멀티스레드 환경
여러 스레드가 동일 json 객체에 접근하면 data race가 발생합니다. 파싱 결과를 스레드별로 복사하거나 불변으로 유지해야 합니다.
1.3 해결 아키텍처
flowchart TB
subgraph 입력
A[HTTP 응답 문자열]
B[파일 스트림]
C[소켓 버퍼]
end
subgraph 파싱
D{라이브러리}
D --> E[nlohmann/json]
D --> F[RapidJSON]
E --> G[json 객체]
F --> G
end
subgraph 검증
G --> H[contains/value 체크]
H --> I[타입 검증 is_*]
I --> J[커스텀 타입 변환]
end
subgraph 출력
J --> K[구조체]
J --> L[비즈니스 로직]
end
A --> D
B --> D
C --> D
2. 라이브러리 비교: nlohmann vs RapidJSON
| 항목 | nlohmann/json | RapidJSON |
|---|---|---|
| 헤더 전용 | ✅ | ✅ |
| API 스타일 | STL 유사, 직관적 | C 스타일, 명시적 |
| 성능 | 보통 | 매우 빠름 |
| 메모리 | 상대적으로 많음 | 적음 (in-place 등) |
| 커스텀 직렬화 | to_json/from_json 간편 | 수동 구현 |
| 에러 처리 | 예외 기반 | 반환값 + HasParseError |
| C++ 표준 | C++11 | C++11 |
| 추천 용도 | REST API, 설정, 프로토타입 | 고성능 서버, 대용량, 임베디드 |
선택 가이드:
- nlohmann: 개발 속도·가독성 우선, 대부분의 웹 API·설정 파싱
- RapidJSON: 초당 수만 건 파싱, 메모리 제한 환경, 로그 파이프라인
3. nlohmann/json 완전 파싱 예제
3.1 설치
# vcpkg
vcpkg install 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)
3.2 기본 파싱: 문자열·파일
// 복사해 붙여넣은 뒤: g++ -std=c++17 -o json_parse json_parse.cpp -I<경로> && ./json_parse
#include <nlohmann/json.hpp>
#include <fstream>
#include <iostream>
#include <string>
using json = nlohmann::json;
int main() {
// 1. 문자열 파싱
std::string str = R"({"name": "Alice", "age": 30, "tags": ["admin", "user"]})";
json j = json::parse(str);
std::string name = j["name"].get<std::string>();
int age = j["age"].get<int>();
std::cout << name << ", " << age << "\n"; // Alice, 30
// 2. 배열 순회
for (auto& tag : j["tags"]) {
std::cout << tag.get<std::string>() << " ";
}
std::cout << "\n"; // admin user
// 3. 파일 파싱 (config.json)
std::ifstream f("config.json");
if (f) {
json config = json::parse(f);
int port = config.value("port", 8080); // 기본값 8080
std::string host = config.value("host", "localhost");
std::cout << "port=" << port << ", host=" << host << "\n";
}
return 0;
}
3.3 안전한 접근 패턴
#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;
void safe_access_example(const json& j) {
// ❌ 위험: 키가 없으면 null 삽입
// auto v = j["optional_key"];
// ✅ contains로 존재 확인
if (j.contains("optional_key")) {
auto v = j["optional_key"];
std::cout << v << "\n";
}
// ✅ value로 기본값 지정 (키 없거나 null이면 default 반환)
int timeout = j.value("timeout", 30);
std::string env = j.value("env", "development");
// ✅ at(): 키 없으면 out_of_range 예외 (검증 필요 시)
try {
std::string required = j.at("required_field").get<std::string>();
} catch (const json::out_of_range& e) {
std::cerr << "필수 필드 누락: " << e.what() << "\n";
}
// ✅ find로 이터레이터 사용
auto it = j.find("maybe_null");
if (it != j.end() && !it->is_null()) {
int val = it->get<int>();
}
}
3.4 중첩 객체·배열 파싱
#include <nlohmann/json.hpp>
#include <iostream>
#include <vector>
using json = nlohmann::json;
struct UserSummary {
int id;
std::string name;
};
std::vector<UserSummary> parse_users_response(const std::string& body) {
json j = json::parse(body);
std::vector<UserSummary> users;
// {"data": {"users": [{"id": 1, "name": "Alice"}, ...]}}
if (!j.contains("data") || !j["data"].contains("users")) {
return users;
}
for (auto& item : j["data"]["users"]) {
if (!item.contains("id") || !item.contains("name")) continue;
users.push_back({
item["id"].get<int>(),
item["name"].get<std::string>()
});
}
return users;
}
int main() {
std::string api_response = R"({
"data": {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
}
})";
auto users = parse_users_response(api_response);
for (const auto& u : users) {
std::cout << u.id << ": " << u.name << "\n";
}
return 0;
}
3.5 에러 처리와 예외
#include <nlohmann/json.hpp>
#include <iostream>
#include <optional>
using json = nlohmann::json;
std::optional<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 std::nullopt;
}
}
int main() {
// 잘못된 JSON: 쉼표 누락
std::string bad = R"({"a": 1 "b": 2})";
auto j = safe_parse(bad);
if (!j) {
std::cout << "파싱 실패, 폴백 처리\n";
return 1;
}
// 타입 에러 방지: is_* 검증
json j2 = json::parse(R"({"age": "thirty"})");
if (j2["age"].is_number_integer()) {
int age = j2["age"].get<int>();
} else {
std::cout << "age가 숫자가 아님\n";
}
return 0;
}
4. RapidJSON 완전 파싱 예제
4.1 설치
# vcpkg
vcpkg install rapidjson
# 또는 헤더만 복사: include/rapidjson 폴더를 프로젝트에
4.2 기본 파싱
// g++ -std=c++17 -o rapidjson_parse rapidjson_parse.cpp -I<경로> && ./rapidjson_parse
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <iostream>
#include <string>
using namespace rapidjson;
int main() {
const char* json = R"({"name": "Alice", "age": 30, "active": true})";
Document doc;
doc.Parse(json);
if (doc.HasParseError()) {
std::cerr << "파싱 오류: " << GetParseError_En(doc.GetParseError())
<< " (offset: " << doc.GetErrorOffset() << ")\n";
return 1;
}
// 객체인지 확인
if (!doc.IsObject()) {
std::cerr << "JSON이 객체가 아님\n";
return 1;
}
// HasMember로 키 존재 확인
if (doc.HasMember("name") && doc["name"].IsString()) {
std::cout << "name: " << doc["name"].GetString() << "\n";
}
if (doc.HasMember("age") && doc["age"].IsInt()) {
std::cout << "age: " << doc["age"].GetInt() << "\n";
}
if (doc.HasMember("active") && doc["active"].IsBool()) {
std::cout << "active: " << (doc["active"].GetBool() ? "true" : "false") << "\n";
}
return 0;
}
4.3 파일 파싱 (스트림)
#include <rapidjson/document.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/error/en.h>
#include <cstdio>
#include <iostream>
int main() {
FILE* fp = fopen("config.json", "rb");
if (!fp) {
std::cerr << "파일 열기 실패\n";
return 1;
}
char readBuffer[65536];
rapidjson::FileReadStream is(fp, readBuffer, sizeof(readBuffer));
rapidjson::Document doc;
doc.ParseStream(is);
fclose(fp);
if (doc.HasParseError()) {
std::cerr << "파싱 오류: " << rapidjson::GetParseError_En(doc.GetParseError()) << "\n";
return 1;
}
if (doc.HasMember("port") && doc["port"].IsInt()) {
std::cout << "port: " << doc["port"].GetInt() << "\n";
}
return 0;
}
4.4 배열·중첩 객체 파싱
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <iostream>
#include <vector>
int main() {
const char* json = R"({
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
]
})";
rapidjson::Document doc;
doc.Parse(json);
if (doc.HasParseError() || !doc.HasMember("users") || !doc["users"].IsArray()) {
std::cerr << "파싱 실패 또는 users 배열 없음\n";
return 1;
}
const auto& users = doc["users"].GetArray();
for (rapidjson::SizeType i = 0; i < users.Size(); ++i) {
const auto& u = users[i];
if (!u.IsObject() || !u.HasMember("id") || !u.HasMember("name")) continue;
int id = u["id"].GetInt();
const char* name = u["name"].GetString();
std::cout << id << ": " << name << "\n";
}
return 0;
}
4.5 RapidJSON으로 JSON 생성
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <iostream>
int main() {
rapidjson::Document doc(rapidjson::kObjectType);
rapidjson::Document::AllocatorType& alloc = doc.GetAllocator();
doc.AddMember("name", "Bob", alloc);
doc.AddMember("age", 25, alloc);
rapidjson::Value tags(rapidjson::kArrayType);
tags.PushBack("admin", alloc);
tags.PushBack("user", alloc);
doc.AddMember("tags", tags, alloc);
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
std::cout << buffer.GetString() << "\n";
// {"name":"Bob","age":25,"tags":["admin","user"]}
return 0;
}
5. 커스텀 타입 직렬화
5.1 nlohmann: to_json / from_json
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <optional>
using json = nlohmann::json;
struct User {
std::string name;
int age;
std::vector<std::string> tags;
std::optional<std::string> email; // 선택적
};
// 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);
}
if (j.contains("email") && !j["email"].is_null()) {
u.email = j["email"].get<std::string>();
}
}
// User → JSON (직렬화)
void to_json(json& j, const User& u) {
j = json{
{"name", u.name},
{"age", u.age},
{"tags", u.tags}
};
if (u.email) {
j["email"] = *u.email;
}
}
int main() {
std::string str = R"({"name": "Alice", "age": 30, "tags": ["admin"], "email": "[email protected]"})";
json j = json::parse(str);
User u = j.get<User>();
std::cout << u.name << ", " << u.age << "\n";
if (u.email) std::cout << "email: " << *u.email << "\n";
json j2 = u; // to_json 자동 호출
std::cout << j2.dump(2) << "\n";
return 0;
}
5.2 NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE (간단한 구조체)
#include <nlohmann/json.hpp>
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>();
5.3 enum 직렬화
#include <nlohmann/json.hpp>
#include <string>
using json = nlohmann::json;
enum class Status { Pending, Active, Done };
void to_json(json& j, Status s) {
switch (s) {
case Status::Pending: j = "pending"; break;
case Status::Active: j = "active"; break;
case Status::Done: j = "done"; break;
}
}
void from_json(const json& j, Status& s) {
std::string v = j.get<std::string>();
if (v == "pending") s = Status::Pending;
else if (v == "active") s = Status::Active;
else s = Status::Done;
}
5.4 RapidJSON 커스텀 타입 (수동)
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <string>
#include <vector>
struct User {
std::string name;
int age;
std::vector<std::string> tags;
};
User parse_user(const rapidjson::Value& v) {
User u;
if (v.HasMember("name") && v["name"].IsString())
u.name = v["name"].GetString();
if (v.HasMember("age") && v["age"].IsInt())
u.age = v["age"].GetInt();
if (v.HasMember("tags") && v["tags"].IsArray()) {
for (auto& t : v["tags"].GetArray()) {
if (t.IsString()) u.tags.push_back(t.GetString());
}
}
return u;
}
void user_to_json(const User& u, rapidjson::Value& v, rapidjson::Document::AllocatorType& alloc) {
v.SetObject();
v.AddMember("name", rapidjson::Value(u.name.c_str(), alloc), alloc);
v.AddMember("age", u.age, alloc);
rapidjson::Value tags(rapidjson::kArrayType);
for (const auto& t : u.tags) {
tags.PushBack(rapidjson::Value(t.c_str(), alloc), alloc);
}
v.AddMember("tags", tags, alloc);
}
6. 자주 발생하는 에러와 해결법
6.1 type_error: 타입 불일치
원인: get<int>() 호출 시 실제 값이 문자열·null·다른 타입
해결:
// ❌ 위험
int age = j["age"].get<int>(); // "age"가 "30"(문자열)이면 type_error
// ✅ 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>());
} else {
// 기본값 또는 에러 처리
}
// ✅ value로 기본값 (nlohmann)
int age = j.value("age", 0);
6.2 out_of_range / null 접근
원인: j["key"]로 존재하지 않는 키 접근 시 null 삽입, 이후 get<T>()에서 실패
해결:
// ❌ 위험
auto v = j["optional_key"];
int x = v.get<int>(); // optional_key 없으면 null → type_error
// ✅ contains + value
if (j.contains("optional_key") && !j["optional_key"].is_null()) {
int x = j["optional_key"].get<int>();
}
int x = j.value("optional_key", 0); // 없으면 0
6.3 parse_error: 잘못된 JSON 문법
원인: 쉼표 누락, 따옴표 미닫힘, 제어 문자 등
해결:
// ✅ try-catch로 파싱 실패 처리
try {
json j = json::parse(response_body);
} catch (const json::parse_error& e) {
std::cerr << "파싱 오류: " << e.what() << " at byte " << e.byte << "\n";
// 로깅, 폴백, 재시도 등
}
6.4 메모리 과다 사용 (대용량 JSON)
원인: 전체 문자열을 메모리에 로드 후 parse → 2배 메모리
해결:
// ✅ 파일은 스트림으로 파싱
std::ifstream f("large.json");
json j = json::parse(f); // 문자열로 먼저 읽지 않음
// ✅ RapidJSON SAX: 이벤트 기반, DOM 없이 스트리밍
// 필요한 필드만 추출하면 메모리 절약
6.5 인코딩 문제 (UTF-8)
원인: JSON은 UTF-8이 기본. Windows 등에서 CP949 등 다른 인코딩으로 저장된 파일 파싱 시 깨짐
해결:
// ✅ 입력을 UTF-8로 보장
// - 파일 저장 시 UTF-8
// - HTTP 응답 Content-Type: application/json; charset=utf-8
// - nlohmann dump(indent, indent_char, false) 로 ensure_ascii=false 시 한글 그대로 출력
6.6 Data Race (멀티스레드)
원인: 여러 스레드가 동일 json 객체에 동시 쓰기/읽기
해결:
// ✅ 파싱 후 스레드별로 복사
json j_global = json::parse(body);
std::thread t([j_global]() { // 복사로 전달
process(j_global);
});
// ✅ 또는 파싱을 스레드 내부에서
std::thread t([body]() {
json j = json::parse(body); // 스레드 로컬
process(j);
});
7. 베스트 프랙티스
7.1 필수 필드 vs 선택 필드 구분
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"];
}
}
7.2 API 응답 래퍼
template<typename T>
struct ApiResult {
bool ok;
T data;
std::string error_msg;
};
template<typename T>
ApiResult<T> parse_api_response(const std::string& body) {
ApiResult<T> result{false, {}, ""};
try {
json j = json::parse(body);
if (j.contains("error")) {
result.error_msg = j.value("error", "unknown");
return result;
}
result.data = j.get<T>();
result.ok = true;
} catch (const json::exception& e) {
result.error_msg = e.what();
}
return result;
}
7.3 설정 파일 로드 패턴
struct Config {
int port = 8080;
std::string host = "0.0.0.0";
int timeout = 30;
};
Config load_config(const std::string& path) {
std::ifstream f(path);
if (!f) throw std::runtime_error("Cannot open: " + path);
json j = json::parse(f);
Config c;
c.port = j.value("port", 8080);
c.host = j.value("host", "0.0.0.0");
c.timeout = j.value("timeout", 30);
return c;
}
7.4 로깅용 한 줄 JSON
// 이벤트·로그 직렬화: 한 줄로 출력 (Kafka, 로그 파일)
json event = {{"ts", timestamp}, {"level", "info"}, {"msg", message}};
std::cout << event.dump() << "\n"; // 압축 형식
8. 성능 비교와 선택 가이드
8.1 벤치마크 (참고)
| 라이브러리 | 1MB JSON 파싱 (ms) | 메모리 (상대) |
|---|---|---|
| nlohmann/json | 1.0x | |
| RapidJSON (DOM) | ~0.5x | |
| RapidJSON (SAX) | ~0.2x |
환경에 따라 수치 변동
8.2 선택 가이드
flowchart TD
A[JSON 파싱 필요] --> B{성능·메모리 제약?}
B -->|아니오| C[nlohmann/json]
B -->|예| D{대용량 스트리밍?}
D -->|예| E[RapidJSON SAX]
D -->|아니오| F[RapidJSON DOM]
C --> G[개발 편의성 우선]
F --> H[고성능, 저메모리]
E --> I[최소 메모리]
9. 프로덕션 패턴
9.1 HTTP 응답 + JSON 파싱 통합
#include <nlohmann/json.hpp>
#include <string>
using json = nlohmann::json;
// HTTP 클라이언트로 body 수신 후 (예: #21-1 참고)
bool fetch_and_parse(const std::string& url, json& out) {
std::string body;
if (!http_get(url, body)) return false;
try {
out = json::parse(body);
return true;
} catch (const json::parse_error& e) {
// 로깅: e.what(), e.byte
return false;
}
}
9.1b 실전: REST API 사용자 목록 조회 (통합 예제)
// HTTP GET + JSON 파싱 + 커스텀 타입 변환 통합
#include <nlohmann/json.hpp>
#include <string>
#include <vector>
#include <optional>
using json = nlohmann::json;
struct User {
int id;
std::string name;
std::optional<std::string> email;
};
std::optional<std::vector<User>> fetch_users(const std::string& host,
const std::string& path) {
std::string body;
if (!http_get(host, path, body)) return std::nullopt;
try {
json j = json::parse(body);
if (!j.contains("data") || !j["data"].is_array()) return std::nullopt;
std::vector<User> users;
for (auto& item : j["data"]) {
if (!item.contains("id") || !item.contains("name")) continue;
User u;
u.id = item["id"].get<int>();
u.name = item["name"].get<std::string>();
if (item.contains("email") && !item["email"].is_null())
u.email = item["email"].get<std::string>();
users.push_back(std::move(u));
}
return users;
} catch (const json::exception&) {
return std::nullopt;
}
}
9.2 재시도 + 파싱
json fetch_with_retry(const std::string& url, int max_retries = 3) {
for (int i = 0; i < max_retries; ++i) {
std::string body;
if (!http_get(url, body)) {
std::this_thread::sleep_for(std::chrono::seconds(1 << i));
continue;
}
try {
return json::parse(body);
} catch (const json::parse_error&) {
// 파싱 실패는 재시도해도 소용없음
throw;
}
}
throw std::runtime_error("HTTP fetch failed after retries");
}
9.3 스키마 검증 (선택)
nlohmann/json 자체에는 스키마 검증이 없습니다. valijson, nlohmann/json-schema 등 별도 라이브러리로 스키마 검증을 추가할 수 있습니다.
9.4 프로덕션 체크리스트
- 파싱 예외 처리:
parse_error,type_errortry-catch - 안전한 접근:
contains,value,at사용 - 타입 검증:
is_*또는 스키마 검증 - 선택 필드:
std::optional,value(key, default) - 대용량: 스트림 파싱 또는 SAX 고려
- 멀티스레드: 파싱 결과 복사 또는 스레드 로컬
- 로깅: 파싱 실패 시 요청/응답 샘플 로깅 (개인정보 제외)
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
- C++ JSON 처리 | nlohmann/json으로 파싱과 생성하기 [#27-2]
- C++ 소켓 프로그래밍 완벽 가이드 | TCP/UDP·소켓 옵션·논블로킹·에러 처리 [#28-1]
이 글에서 다루는 키워드 (관련 검색어)
C++ JSON 파싱, nlohmann/json, RapidJSON, REST API 응답 파싱, JSON 직렬화, 커스텀 타입 JSON 등으로 검색하시면 이 글이 도움이 됩니다.
정리
| 항목 | 내용 |
|---|---|
| nlohmann | STL 유사 API, to_json/from_json, 개발 편의성 |
| RapidJSON | 고성능, 저메모리, SAX 스트리밍 |
| 안전 접근 | contains, value, at, is_* 검증 |
| 에러 | parse_error, type_error, out_of_range 처리 |
| 커스텀 타입 | to_json/from_json, NLOHMANN_DEFINE_TYPE |
| 프로덕션 | 예외 처리, 재시도, 로깅, 스키마 검증 |
실전에서는:
- 대부분의 경우 nlohmann으로 충분
- 고성능·대용량은 RapidJSON
- 항상 안전한 접근 패턴 적용
자주 묻는 질문 (FAQ)
Q. nlohmann과 RapidJSON 중 뭘 써야 하나요?
A. 개발 편의성·가독성은 nlohmann, 고성능·저메모리가 필요하면 RapidJSON. 대부분의 서비스는 nlohmann으로 충분합니다.
Q. JSON 파싱이 느려요.
A. RapidJSON으로 전환, 스트림 파싱, 필요한 필드만 추출(SAX), 파싱 결과 캐싱을 고려하세요.
Q. 선행으로 읽으면 좋은 글은?
A. HTTP 클라이언트(#21-1)로 API 응답을 받은 뒤 이 글에서 JSON 파싱을 적용할 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인하세요.
한 줄 요약: nlohmann/json과 RapidJSON으로 JSON을 안전하게 파싱하고, contains·value·커스텀 직렬화로 프로덕션 수준의 에러 처리를 적용할 수 있습니다.
다음 글: [C++ 실전 가이드 #22-1] Concepts 기초: 타입 제약과 requires
이전 글: [C++ 실전 가이드 #21-1] HTTP 클라이언트 완벽 가이드
참고 자료
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |