1. 소개
이전 강의들에서는 항상 강한 형식의 구조로 작업했어요. 예를 들어, Person 클래스가 있고 이를 JSON으로 직렬화했다가 다시 역직렬화한다고 합시다:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
하지만 때때로 데이터 구조를 미리 알 수 없을 때가 있습니다. 예를 들어:
- 응답 구조가 고정되어 있지 않은 외부 서비스용 파서를 작성할 때.
- 전체 클래스를 채우지 않고 일부 정보만 추출해야 할 때.
- 동적 조건에 따라 JSON을 "런타임에" 수정하거나 생성해야 할 때.
이럴 때 "동적 구조"가 필요합니다 — 키와 값의 집합으로 JSON 트리를 보관하는 객체들로, 미리 C# 클래스를 정의할 필요가 없습니다.
왜 보통 Newtonsoft.Json을 쓰는가
.NET에서 JSON 작업을 위한 주요 플레이어는 두 가지입니다:
- System.Text.Json — Microsoft의 내장 라이브러리(.NET Core 3.0부터 발전).
- Newtonsoft.Json (Json.NET) — 인기 있는 라이브러리로, JObject와 JArray 같은 클래스를 제공합니다.
이 강의 작성 시점에서는 System.Text.Json이 JObject/JArray와 같은 편의성을 완전히 제공하지 않습니다. 따라서 복잡하거나 알려지지 않은 JSON 구조를 자주 파싱하거나 수정해야 한다면 보통 Newtonsoft.Json을 선택합니다.
주요 타입: JObject, JArray, JValue 와 JToken 계열
- JToken — 모든 JSON 노드의 기본 타입입니다.
- JObject — JSON 객체 { ... }, "키-값" 쌍의 집합.
- JArray — 배열 [ ... ].
- JValue — 단일 값 (42, "텍스트", true, null 등).
아이디어는 단순합니다: JSON을 파싱하면 이 토큰들로 이루어진 "트리"를 얻고, 그 위를 이동하면서 찾고, 바꾸고, 삭제하고, 추가할 수 있습니다.
2. 구조를 모르는 JSON 읽기
예를 들어 다음 같은 JSON이 들어왔고 미리 클래스를 작성하고 싶지 않다고 합시다:
{
"status": "ok",
"amount": 150.5,
"items": [
{
"name": "book",
"qty": 1
},
{
"name": "pen",
"qty": 3
}
]
}
Newtonsoft.Json을 사용하면 이걸 트리로 바꿔 런타임에 탐색할 수 있습니다.
예제: JSON을 JObject로 읽기
using Newtonsoft.Json.Linq;
string json = @"{
""status"": ""ok"",
""amount"": 150.5,
""items"": [
{ ""name"": ""book"", ""qty"": 1 },
{ ""name"": ""pen"", ""qty"": 3 }
]
}";
// 문자열을 파싱해서 트리를 얻음
JObject root = JObject.Parse(json);
// 사전처럼 속성에 접근
string status = (string)root["status"]; // "ok"
double amount = (double)root["amount"]; // 150.5
// items는 배열이므로 JArray
JArray items = (JArray)root["items"];
// 배열을 순회
foreach (JObject item in items)
{
string name = (string)item["name"];
int qty = (int)item["qty"];
Console.WriteLine($"상품: {name}, 수량: {qty}");
}
클래스를 선언할 필요 없이 필요한 부분만 빠르게 긁어오는 데 아주 편합니다.
3. 인덱서와 동적 접근
인덱서:
- 객체용: root["status"], root["items"]
- 배열용: items[0], items[1]
바로 원하는 타입으로 얻으려면 (string), (int), (bool), (double) 같은 캐스팅을 사용하세요 — 라이브러리가 타입을 자동으로 변환해줍니다.
데이터가 없을 수도 있다면 조심해야 합니다: 존재하지 않는 키에 접근하면 null을 반환하고, 명시적 캐스팅은 예외를 던집니다. 체크하는 메서드를 쓰는 편이 안전합니다:
if (root.TryGetValue("amount", out var token))
{
double amount = token.Value<double>();
// 이 방식이 편리: Value<T>()는 바로 변환해줌
}
중첩된 객체나 배열도 간단히 읽을 수 있습니다:
// 두번째 아이템 얻기
JObject secondItem = (JObject)root["items"][1];
string itemName = (string)secondItem["name"]; // "pen"
동적 방식으로도 할 수 있습니다:
dynamic droot = root;
Console.WriteLine(droot.status); // "ok"
하지만 기억하세요: dynamic을 쓰면 컴파일러가 필드 접근을 검사하지 않아서, 오류는 런타임에만 드러납니다.
4. JSON 트리를 런타임에 수정하기
요소 추가
root["currency"] = "RUB"; // 새 속성 추가
items.Add(new JObject
{
["name"] = "eraser",
["qty"] = 2
});
수정과 삭제
root["status"] = "done"; // 값 수정
items[0]["qty"] = 5; // 첫 번째 상품 수량 증가
items.RemoveAt(1); // 두 번째 상품 삭제
최종 문자열로 저장
string modifiedJson = root.ToString();
// 보기 좋게 하려면 root.ToString(Formatting.Indented)
5. 처음부터 JSON 구조 만들기
var person = new JObject
{
["name"] = "Alice",
["age"] = 22,
["languages"] = new JArray { "C#", "Python" },
["isStudent"] = true
};
Console.WriteLine(person.ToString(Newtonsoft.Json.Formatting.Indented));
{
"name": "Alice",
"age": 22,
"languages": [
"C#",
"Python"
],
"isStudent": true
}
외부 API에 일부 데이터만 반환하거나 조건에 따라 JSON을 조립할 때 유용합니다.
6. JObject에서 자신의 객체를 조립하는 방법
유연한 JSON을 강한 타입의 C# 객체로 바꿔야 할 때가 있습니다. 방법:
- 일반적인 역직렬화: JsonConvert.DeserializeObject<MyClass>(...).
- JObject에서 필요한 필드를 꺼내 수동으로 객체를 조립.
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// 예: 이런 JSON이 들어옴
string incoming = @"{ ""name"":""Bob"", ""age"":30, ""extraField"":true }";
JObject j = JObject.Parse(incoming);
// 수동으로 객체 조립 — 필요한 필드만 사용
var person = new Person
{
Name = (string)j["name"],
Age = (int)j["age"],
// extraField는 필요 없으니 무시
};
7. 예제 오류와 주의점: 흔한 함정
동적 JSON 구조 작업은 유연하지만 몇 가지 "함정"이 있습니다:
- 존재하지 않는 키/요소에 접근하면 null이 나오고, 값 타입으로의 명시적 캐스팅은 예외를 발생시킵니다.
- 예상 타입과 실제 타입이 다르면(예: 객체를 기대했는데 값이 온 경우) 캐스팅 에러가 납니다.
- 객체의 필드를 순회하려면 Properties()를 사용하세요:
foreach (var prop in root.Properties())
{
Console.WriteLine($"필드: {prop.Name}, 값: {prop.Value}");
}
- Newtonsoft.Json은 LINQ 스타일 쿼리(필터링, 검색 등)로 다루기도 편합니다:
var expensiveItems = items.Where(obj => (int)obj["qty"] > 2);
foreach (var item in expensiveItems)
Console.WriteLine(item);
8. JObject/JArray 작업 시 흔한 실수
실수 #1: 기대한 필드가 없거나 타입이 맞지 않는 경우. 개발자가 어떤 필드가 있을 거라 가정했는데, 없거나 다른 타입일 수 있습니다. 객체를 기대했는데 숫자가 오면 캐스팅 시 예외가 발생합니다. 사용 전에 존재 여부와 타입을 체크하세요.
실수 #2: 중첩 구조의 필드에 null 체크 없이 접근하는 경우. JSON에 중첩 객체가 있고 키가 다르거나 없는 경우, 없는 필드에 접근하면 런타임 오류가 날 수 있습니다. 중첩 노드를 읽기 전에 null 체크를 하세요.
실수 #3: 값이 null인데 값 타입으로 캐스팅하는 경우. 키는 존재하지만 값이 null이면 (int)j["age"] 같은 표현은 예외를 냅니다. Value<T>()를 사용하면 기본값을 반환합니다(예: int는 0, 문자열은 null 등).
실수 #4: 명확한 모델 대신 동적 객체에 지나치게 의존하는 경우. 지나치게 복잡한 구조는 C# 클래스로 명확히 표현하는 게 더 안전하고 가독성이 좋습니다. 구조가 정말로 불확실하거나 크게 변동할 때만 동적 방식을 사용하세요.
이제 JObject와 JArray를 이용해 미리 정의된 모델 없이도 JSON 구조를 빠르고 안전하게 파싱, 수정, 생성하는 방법을 알게 되었습니다.
GO TO FULL VERSION