모던 C++에서 선언적 JSON 디스패치
It looks like only the source link was provided and the article content is missing. Could you please share the text you’d like translated? Once I have the content, I’ll translate it into Korean while preserving the formatting, markdown, and code blocks as requested.
Working with JSON in C++
“C++에서 JSON을 다루는 것은 이제 단순히 바이트를 파싱하는 문제가 아닙니다. nlohmann/json과 같은 성숙한 라이브러리는 이미 JSON 값을 견고하고 동적으로 표현합니다. 실제 도전 과제는 파싱 위의 한 단계 위에 있습니다: 그 값을 해석하는 제어 흐름을 어떻게 구조화하느냐가 핵심입니다.”
작은 예제에서는 if / else if 를 순차적으로 나열하는 것이 충분히 작동합니다. 그러나 로직이 재귀적이 되거나 의미적으로 미묘해질 때—예를 들어 빈 배열과 비어 있지 않은 배열을 구분하거나, 특정 구조적 특성을 가진 객체를 인식해야 할 때—코드는 분기 소음이 쌓이기 시작합니다. 분류, 분기, 동작이 서로 긴밀히 결합되어, 제어 흐름을 지역적으로 이해하기 어려워집니다.
목표
“조건 로직을 최적화하는 방법을 묻기보다, 디스패치를 선언적으로 기술할 수 있는지, 기존의 동적 JSON 모델을 그대로 활용하면서 물어보라.”
여기서 논의된 전체 구현은 다음에 있습니다
🔗
아래에 필수적인 부분만 표시됩니다.
선언적 디스패치가 필요한 이유?
- Dynamic JSON ≈ Sum Type –
nlohmann::json은 동적으로 타입이 지정되지만, 런타임 동작은 실제로 합 타입과 동일합니다: 값은 null, boolean, number, string, array, object 중 정확히 하나의 상호 배타적인 대안입니다. - Current Pain Points
- 조건 체인은 케이스가 늘어날수록 확장성이 떨어집니다.
- Visitor‑style 설계는 매칭과 동작 사이의 의미적 결합을 해결하지 못하고 간접성과 보일러플레이트를 추가합니다.
- Variant‑based 디스패치는 JSON을 폐쇄된 대수 데이터 타입으로 변환하도록 강요하는데, 이는 실무에서 자주 비현실적입니다.
우리가 필요한 것은 기본 데이터 모델을 교체하지 않으면서, 의미적 케이스들의 조합으로 디스패치 로직을 직접 기술할 수 있는 방법입니다.
매치‑파이프라인 접근법
void parse_json(const json& j, int depth = 0) {
match(j)
.when(bind()[is_type(json::value_t::null)] >> print_null(depth))
.when(bind()[is_type(json::value_t::boolean)] >> print_bool(depth))
.when(bind()[is_int] >> print_int(depth))
.when(bind()[is_uint] >> print_uint(depth))
.when(bind()[is_float] >> print_float(depth))
.when(bind()[is_type(json::value_t::string)] >> print_string(depth))
.when(bind()[is_type(json::value_t::array)] >> handle_array(depth))
.when(bind()[has_field("name")] >> handle_named_object(depth))
.otherwise([=] {
indent(depth);
std::cout << "\n";
});
}
이것이 제공하는 것
| 관점 | 전통적인 코드 | 선언적 매치 |
|---|---|---|
| 분류 | 제어 흐름과 뒤섞여 있음 | 명시적 술어 (is_type, has_field, …) |
| 바인딩 | 암시적, 흩어짐 | bind()가 명시적으로 만듭니다 |
| 동작 | 매칭 로직과 혼합 | 핸들러 (print_null, handle_array, …)는 순수 동작입니다 |
match 표현식 자체는 디스패치 전략에 대한 선언적 설명이며, 분기들의 인코딩된 순서가 아닙니다.
설계 제약조건
- 암시적 바인딩 금지 – 데이터가 필요한 모든 것은
bind()를 통해 명시적으로 바인딩해야 합니다. - 3단계 분리
- 매칭 – 케이스가 적용되는지를 판단합니다.
- 바인딩 – 핸들러에 전달될 데이터를 캡처합니다.
- 핸들링 – 부수 효과(출력, 재귀 등)를 수행합니다.
가드(guards)는 바인딩 후에 평가되어, 프레디케이트가 상징적 자리표시자가 아닌 구체적인 값에 대해 작동하도록 보장합니다. 따라서 핸들러 시그니처는 안정적이고 예측 가능하며, 각 핸들러는 매칭 조건이 얼마나 복잡하든 const json& 하나의 인자만 받습니다.
Predicates
inline auto is_type(json::value_t t) {
return [=](const json& j) { return j.type() == t; };
}
inline auto has_field(const std::string& key) {
return [=](const json& j) {
return j.is_object() && j.contains(key);
};
}
match 표현식 내에서 이러한 프레디케이트는 제어 흐름 역할이 아니라 의미적 역할을 수행합니다. 가드는 케이스의 의미를 정제하며, 실행 가능한 분기를 나타내는 것이 아닙니다. 프레디케이트가 더 복잡해지거나 여러 케이스에서 재사용될 때 이 구분은 매우 중요해집니다.
핸들러 – 순수 동작
inline auto print_null(int depth) {
return [=](auto&&...) {
indent(depth);
std::cout << "null\n";
};
}
핸들러의 시그니처는 그 의도를 반영합니다: 특정 데이터 형태가 아니라 의미적 카테고리에 응답합니다.
이러한 디커플링을 통해 핸들러는 매칭 구조와 독립적으로 진화할 수 있으며, 시스템의 어느 부분이 어떤 결정을 담당하는지 명확해집니다.
재귀 순회
inline auto handle_array(int depth) {
return [=](const json& j) {
indent(depth);
std::cout << "[\n";
for (const auto& elem : j) {
parse_json(elem, depth + 1);
}
indent(depth);
std::cout << "]\n";
};
}
( handle_named_object, print_int, print_string 등과 같은 추가 핸들러도 동일한 패턴을 따릅니다.)
요약
- Declarative match pipelines는 각 JSON 형태에 대해 무엇을 해야 하는지를 설명하게 해 주며, 어떻게 도달할지를 설명하지 않습니다.
- matching, binding, 그리고 handling을 분리함으로써 코드는 읽기 쉽고, 확장 가능하며, 복잡한 제어 흐름 구조에서 자유롭게 유지됩니다.
- 이 접근법은 동적
nlohmann::json모델에서 직접 작동하여 JSON을 닫힌 대수적 데이터 타입으로 변환할 필요가 없습니다.
링크된 저장소에서 전체 샘플 구현을 자유롭게 살펴보세요. 즐거운 디스패치 되세요!
추가 예시 (발췌)
inline auto handle_array_verbose(int depth) {
return [=](const json& arr) {
indent(depth);
std::cout << "array [" << arr.size() << "]\n";
for (const auto& v : arr) {
parse_json(v, depth + 1);
}
};
}
재귀는 명시적이며 지역적이고 예상 가능하다. 숨겨진 제어 흐름이나 순회와 디스패치 전략 사이의 암시적 결합이 없다.
이 접근법의 장점은 문법적인 새로움이 아니다. 그것은 강제하는 규율에 있다:
- 분류는 패턴에 국한된다.
- 의미적 제약은 가드(guard)로 표현된다.
- 동작은 핸들러에 한정된다.
새로운 경우가 도입될 때—추가적인 구조적 구분, 스키마와 같은 검사, 혹은 특수한 해석—매치 표현식은 수평적으로 확장될 뿐, 더 깊게 중첩되지 않는다.
흥미로운 확장은 추출자 패턴이다: 구조를 검증하고 하위 구성 요소를 직접 바인딩하는 패턴. JSON 컨텍스트에서 이러한 패턴은 특정 필드를 가진 객체와 매치하고 그 필드들을 독립적인 값으로 바인딩할 수 있어, 구조적 분해를 핸들러가 아니라 패턴 계층 자체로 옮긴다.
전체 구현(모든 프레디케이트와 핸들러 포함)은 다음에서 확인할 수 있다.
https://github.com/sentomk/patternia/tree/main/samples/json_dispatch