JSON DTO Converter (2) - JSON 분석
JsonValidator → JsonNode → SchemaNode → TypeInferencer → ModelGraph
"JSON이 어떻게 DTO 설계도로 변환되는가?"를 깊이 있게 분석한다
이 글은 JSON DTO Converter의 핵심 로직이 담긴 "JSON 분석 계층"을 집중적으로 다룬다.
이 계층은 도구 전체의 심장이다.
- JSON 파일을 로드하고
- 구조를 분석하여
- Java 타입을 추론하고
- DTO 클래스 구조(ModelGraph)를 만드는
모든 과정이 여기에 포함된다.
0. JSON 분석 계층이 왜 중요한가?
CLI 계층이 "입력 검증"을 담당했다면,
JSON 분석 계층은 실제로 JSON을 분석하여 DTO 설계도를 만들어내는 엔진이다.
즉, JSON 분석 계층은 다음 역할을 한다:
JSON 파일 → JsonValidator
JsonNode 트리 → JsonAnalyzer
SchemaNode 스키마 → TypeInferencer
Java 타입 → ModelGraph
DTO ClassSpec → Generator 계층 전달
여기서 잘못되면 올바른 DTO를 만들 수 없다.
그래서 이 계층이 가장 중요하며, 가장 정교하게 설계되어야 한다.
1. JsonValidator - JSON 파일을 안전하게 로드하기 위한 필터
JsonValidator는 프로그램 전체에서 가장 먼저 JSON 파일을 직접 다루는 계층이다.
✔ JsonValidator가 수행하는 검증 리스트
1) 파일 존재 여부 확인
- 경로가 존재해야 한다
- 존재하지만 디렉터리라면 금지
2) 파일 크기 제한 (5MB)
왜 제한을 둘까?
- JSON이 지나치게 크면 메모리 사용량이 폭증한다
- CLI 도구는 즉각적인 응답성이 중요하다
- 파일 크기가 비정상적으로 크다면 사용자의 입력 실수 가능성 ↑
따라서 5MB 제한은 "안전 장치"이다.
3) BOM(Byte Order Mark) 검사 및 제거
💡 BOM이란?
일부 Windows 에디터는 UTF-8 파일을 저장할 때
맨 앞에 다음 3바이트를 붙인다:
EF BB BF
이게 BOM이다.
문제는 JSON의 시작은 {여야 하는데
BOM이 있으면…
{
처럼 보이지 않는 쓰레기 바이트가 앞에 붙는다.
파서에 따라 오류가 날 수 있으므로 반드시 제거해야 한다.
JsonValidator의 처리 방식:
- 파일을 raw byte로 읽는다
- 첫 3바이트가 BOM인지 검사
- 맞으면 제거
- 제거 여부를 boolean hadBom 에 기록
이것이 JsonValidator.Result 내부에 포함된다.
4) JSON 문법 파싱
Jackson ObjectMapper.readTree() 사용.
5) 루트 타입 검사
루트는 반드시 ObjectNode여야 한다.
배열 루트는 지원하지 않는다.
✔ JsonValidator.Result
JsonValidator는 다음 데이터를 함께 반환한다:
| 필드 | 설명 |
| JsonNode root | JSON 루트 트리 |
| boolean hadBom | BOM 제거 여부 |
| sizeBytes | 파일 크기 |
이 값들은 이후 JSON 분석 단계에서 활용된다.
2. JsonNode Tree Model — JSON을 메모리 트리로 표현하는 방식
JsonNode는 Jackson이 제공하는 트리 기반 JSON 모델이다.
예를 들어 아래 JSON을 보자:
{
"user": {
"name": "Alice",
"age": 20
}
}
JsonNode 트리 구조는 텍스트가 아니라 실제로 이렇게 생겼다:
ObjectNode
└─ "user" → ObjectNode
├─ "name" → TextNode("Alice")
└─ "age" → IntNode(20)
JsonNode를 사용하는 이유
1) 구조적 탐색에 최적화
필드 키, 값, 노드 타입 등에 대해 자유롭게 탐색 가능.
2) JSON 스키마 분석을 정확하게 수행 가능
값이 객체인지, 배열인지, 숫자인지 쉽게 판단할 수 있음.
3) "스트리밍(JSON parser)"보다 더 안정적
- 스트리밍 파서는 순차 처리라 구조 분석이 어렵다
- DTO 구조 생성에는 트리 기반 파싱(Tree Model)이 필수
즉, JsonNode는 "스키마 분석용 내부 AST(Abstract Syntax Tree)"라고 볼 수 있다.
3. SchemaNode — 도구 내부 JSON 스키마 표현 모델
JsonAnalyzer는 JsonNode를 기반으로 JSON 구조를
언어 독립적인 계층 구조로 추출한다.
이것이 SchemaNode다.
✔ SchemaNode 구조 요약
SchemaNode
├─ SchemaPrimitive : 문자열/숫자/boolean/null
├─ SchemaObject : 필드 이름 → FieldInfo
├─ SchemaArray : 요소 타입의 Set
└─ SchemaUnion : 혼합된 여러 타입 (예: [1, "text"])
각 타입은 서로 조합될 수 있다.
3-1. SchemaPrimitive
JSON 원시값에 해당한다.
| JSON 값 | SchemaPrimitive 타입 |
| "str" | STRING |
| 123 | INTEGER |
| 12.3 | NUMBER |
| true | BOOLEAN |
| null | NULL |
3-2. SchemaObject
예:
{
"name": "Alice",
"age": 20
}
SchemaObject:
SchemaObject
├─ name → FieldInfo(STRING)
└─ age → FieldInfo(INTEGER)
여기서 FieldInfo는 옵셔널 여부까지 계산한다.
▷ presenceCount
JSON 여러 개를 분석할 때(배열 등),
각 필드가 얼마나 등장했는지를 센다.
▷ optional 판단
예를 들어,
[{ "name": "A" }, { "name": "B", "age": 20 }]
출현 횟수:
| 필드 | count | total | optional |
| name | 2 | 2 | false |
| age | 1 | 2 | true |
3-3. SchemaArray
배열의 요소 타입을 모두 모아 Set으로 저장한다.
예:
["A", "B", "C"]
→ SchemaArray{ STRING }
예:
[ true, 42 ]
→ SchemaArray{ BOOLEAN, INTEGER }
→ 이 경우 SchemaUnion으로 변환됨
3-4. SchemaUnion — 값이 여러 타입일 때
예:
{ "value": [1, "A", true] }
분석:
value → SchemaUnion{
INTEGER,
STRING,
BOOLEAN
}
이는 TypeInferencer 단계에서
공통 상위 타입(Object) 또는 Wrapper 타입으로 축소된다.
4. TypeInferencer — SchemaNode → Java 타입 매핑
이제 스키마 구조를 바탕으로 Java 실제 타입을 추론한다.
주요 매핑 규칙
1) Primitive 매핑
| SchemaPrimitive | Java 타입 |
| STRING | String |
| INTEGER | int |
| NUMBER | double |
| BOOLEAN | boolean |
| NULL | Object (또는 nullable wrapper) |
2) Optional 필드 → Wrapper 승격
예:
int → Integer
boolean → Boolean
이는 Java에서는 primitive 타입이 null을 허용하지 않기 때문이다.
3) SchemaArray → List<T>
예:
{ "tags": ["dev", "cs"] }
→ List<String>
4) SchemaObject → 별도 DTO 클래스
이는 ModelGraph에서 클래스로 승격된다.
5) SchemaUnion → 공통 타입으로 수렴
예:
[1, 2, 3] → int
[1, 1.5] → double
[1, "a"] → Object
5. ModelGraph — 최종 DTO 클래스 설계도(IR)
ModelGraph는 TypeInferencer의 결과를 받아서
실제로 생성될 클래스들의 관계도를 만든다.
클래스 생성 전략
1) SchemaObject마다 하나의 ClassSpec 생성
예:
Root
├─ User
└─ Address
2) inner-classes 옵션 반영
- true → Root 클래스 내부에 static class로 생성
- false → 모두 독립된 .java 파일로 생성
ModelGraph가 해결하는 문제들
✔ 클래스 이름 충돌
동일한 이름이 나오면 suffix를 붙인다:
Location, Location2
✔ import 목록 관리
필드 타입이 다른 클래스라면 import
동일 패키지는 생략
✔ 클래스 계층 유지
중첩 구조 유지
6. JSON 분석 전체 흐름을 하나의 예시로 정리
아래 JSON을 예로 들어 과정 전체를 시각화해보자:
{
"user": {
"name": "Alice",
"age": 20
}
}
6-1. JsonNode 트리
ObjectNode
└─ user : ObjectNode
├─ name : "Alice"
└─ age : 20
6-2. SchemaNode 트리
SchemaObject
└─ user : SchemaObject
├─ name : STRING
└─ age : INTEGER
6-3. TypeInferencer 결과
- user → User 클래스
- name → String
- age → int
6-4. ModelGraph 결과
RootDto
└─ User user
User
├─ String name
└─ int age
7. 이 설계의 장점 — 유지보수성과 확장성
✔ 1) JSON 형식만 바뀌어도 DTO 생성 자동 대응
DTO 작성 자동화
✔ 2) 다양한 JSON 구조에 미래 대응
- 가변 필드
- optional 필드
- union 타입
- 중첩 구조
✔ 3) 구조 변경에 강하다
스키마 → 타입 → 클래스 구조
각 단계가 독립되어 있어 유지보수가 쉽다.
8. JSON 분석 파트 요약
| 단계 | 설명 |
| JsonValidator | 파일 검증, BOM 제거, JSON 파싱 |
| JsonNode | 실제 JSON 트리 모델 |
| SchemaNode | 언어 독립적 스키마 구조 |
| TypeInferencer | SchemaNode → Java 타입 추론 |
| ModelGraph | DTO 클래스 구조 결정 |
이 다섯 단계가 JSON DTO Converter의 핵심 엔진이다.
다음 편에서는 JSON을 구조적으로 해석하는
JsonAnalyzer와 TypeInferencer의 내부 흐름을 살펴본다