C#에서 JSON 처리 : Class 정의의 비효율성과 그 대안에 관해 (Newtonsoft.Json)

JSON은 그 형식의 단순함과 유연성 덕분에 요새는 정말 많은곳에서 사용하고 있습니다. 특히 웹 개발에선 표준(?)이 아닐까 싶을 정도로 JSON을 자주 사용합니다. 이러한 추세에 맞춰 C#에서는 최신 .NET에서 JSON 관련 기능을 강화해왔으며, 특히 JSON을 직렬화 및 역직렬화하는 데 있어 Class를 정의하는 것을 권장하고 있습니다.

C#을 사용하여 JSON 직렬화 및 역직렬화 - .NET

그러나 현업에서 수많은 클라이언트-서버 간 데이터를 처리하면서 JSON 특유의 유연함과 다양한 구조에 의해 이러한 방식이 효율적인가?하는 의문이 들었습니다. 이 글은 이런 저의 생각을 공유하고 피드백을 받고 싶어 작성하게 되었습니다.

0. 예제

설명을 위해 현업에서 사용하고 있는 데이터를 약간 가공해 보았습니다. 너무 복잡하지도, 단순하지도 않은 구조를 가진 JSON 입니다.

{
  "ScreeningDataInfo": {
    "ShopName": "닷넷데브",
    "FileName": "닷넷데브 2024-08-01 심사자료",
    "EDealerRequestDate": "",
    "Category": "",
    "StartDate": "2023-08-01",
    "EndDate": "2024-07-31"
  },
  "ScreeningDataList": 
  [
    {
      "ShopID": "닷넷데브",
      "MallID": "닷넷데브sub",
      "MallTypeCode": "32",
      "VendorCode": "KB",
      "SalesReturnData": [
        {
          "Year": "2023",
          "Month": "07",
          "SalesAmount": 2185820.0,
          "ReturnAmount": 88400.0,
          "ReturnRate": 4.04
        },
        {
          "Year": "2023",
          "Month": "08",
          "SalesAmount": 708540.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        },
        {
          "Year": "2023",
          "Month": "09",
          "SalesAmount": 1212910.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        },
        {
          "Year": "2023",
          "Month": "10",
          "SalesAmount": 604500.0,
          "ReturnAmount": 19840.0,
          "ReturnRate": 3.28
        },
        {
          "Year": "2023",
          "Month": "11",
          "SalesAmount": 782350.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        },
        {
          "Year": "2023",
          "Month": "12",
          "SalesAmount": 674390.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        },
        {
          "Year": "2024",
          "Month": "01",
          "SalesAmount": 1038410.0,
          "ReturnAmount": 28930.0,
          "ReturnRate": 2.78
        },
        {
          "Year": "2024",
          "Month": "02",
          "SalesAmount": 344400.0,
          "ReturnAmount": 95100.0,
          "ReturnRate": 27.61
        },
        {
          "Year": "2024",
          "Month": "03",
          "SalesAmount": 145620.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        },
        {
          "Year": "2024",
          "Month": "04",
          "SalesAmount": 550370.0,
          "ReturnAmount": 59860.0,
          "ReturnRate": 10.87
        },
        {
          "Year": "2024",
          "Month": "05",
          "SalesAmount": 224300.0,
          "ReturnAmount": 27800.0,
          "ReturnRate": 12.39
        },
        {
          "Year": "2024",
          "Month": "06",
          "SalesAmount": 0.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        },
        {
          "Year": "2024",
          "Month": "07",
          "SalesAmount": 0.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        },
        {
          "Year": "2024",
          "Month": "08",
          "SalesAmount": 499370.0,
          "ReturnAmount": 0.0,
          "ReturnRate": 0.0
        }
      ],
      "FirstExpectPayAmount": 8651050.0,
      "MallTypeNameKor": "닷넷데브",
      "MallType": 32,
      "Total" : {
        "SalesAmount": 8970980.0,
        "ReturnAmount": 319930.0,
        "ReturnRate": 3.56
      }
    }
  ]
}

1. Class를 생성하는 것의 비효율성

주어진 JSON 데이터 구조를 반영하기 위해서는 여러 개의 클래스가 필요합니다. 왜냐하면 각 중첩된 객체와 배열에 대해 별도의 클래스를 만들어야 하기 때문입니다.

예를 들어, 다음과 같은 클래스들이 필요할 것입니다.


public class ScreeningDataInfo
{
    public string ShopName { get; set; }
    public string FileName { get; set; }
    public string EDealerRequestDate { get; set; }
    public string Category { get; set; }
    public string StartDate { get; set; }
    public string EndDate { get; set; }
}

public class SalesReturnData
{
    public string Year { get; set; }
    public string Month { get; set; }
    public double SalesAmount { get; set; }
    public double ReturnAmount { get; set; }
    public double ReturnRate { get; set; }
}

public class ScreeningDataList
{
    public string ShopID { get; set; }
    public string MallID { get; set; }
    public string MallTypeCode { get; set; }
    public string VendorCode { get; set; }
    public SalesReturnData[] SalesReturnData { get; set; }
}

public class Total
{
    public double SalesAmount { get; set; }
    public double ReturnAmount { get; set; }
    public double ReturnRate { get; set; }
}

public class Data
{
    public ScreeningDataInfo ScreeningDataInfo { get; set; }
    public ScreeningDataList[] ScreeningDataList { get; set; }
    public double FirstExpectPayAmount { get; set; }
    public string MallTypeNameKor { get; set; }
    public int MallType { get; set; }
    public Total Total { get; set; }
}

이 클래스들을 볼 때 제가 생각하는 문제는 크게 3가지입니다.

  1. 클래스의 중복 생성: JSON 구조가 복잡할수록 많은 클래스가 필요합니다. 이러한 클래스들은 특정 JSON 형식에 의존하기 때문에 재사용성이 낮고, 유사한 구조에 대해서도 새로운 클래스를 작성해야 할 수 있습니다.
  2. 유지보수 어려움: JSON 구조가 변경될 때마다 관련된 클래스들을 수정해야 하며, 이는 유지보수의 부담을 가중시킵니다. 예를 들어, JSON에 새로운 필드가 추가되면 해당 필드를 포함하도록 클래스를 수정해야 합니다.
  3. 비효율적인 메모리 사용: 클래스 생성과 관리에는 메모리와 성능 비용이 수반됩니다. 특히, 필요하지 않은 클래스까지 생성하게 되는 경우 불필요한 리소스 낭비로 이어질 수 있습니다.

2. Newtonsoft.Json을 활용한 직렬화/역직렬화의 장점

반면, Newtonsoft.Json 라이브러리를 사용하면 클래스 정의 없이도 JSON 데이터를 다룰 수 있으며, 매우 간편하게 JSON을 처리할 수 있습니다.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

var json = @"
{
  'ScreeningDataInfo': {
    'ShopName': '닷넷데브',
    'FileName': '닷넷데브 2024-08-01 심사자료',
    'EDealerRequestDate': '',
    'Category': '',
    'StartDate': '2023-08-01',
    'EndDate': '2024-07-31'
  },
  'ScreeningDataList': [
    {
      'ShopID': '닷넷데브',
      'MallID': '닷넷데브sub',
      'MallTypeCode': '32',
      'VendorCode': 'KB',
      'SalesReturnData': [
        {
          'Year': '2023',
          'Month': '07',
          'SalesAmount': 2185820.0,
          'ReturnAmount': 88400.0,
          'ReturnRate': 4.04
        },
        ...
      ]
    }
  ],
  'FirstExpectPayAmount': 8651050.0,
  'MallTypeNameKor': '닷넷데브',
  'MallType': 32,
  'Total': {
    'SalesAmount': 8970980.0,
    'ReturnAmount': 319930.0,
    'ReturnRate': 3.56
  }
}";

// JSON을 dynamic 객체로 역직렬화
dynamic data = JsonConvert.DeserializeObject(json);

// 원하는 데이터에 바로 접근 가능
string shopName = data.ScreeningDataInfo.ShopName;
double totalSales = data.Total.SalesAmount;

제가 생각하는 Newtonsoft.Json의 장점은 아래와 같습니다.

  1. 유연한 데이터 접근: dynamic이나 JObject를 사용하면, JSON 구조에 맞춰 클래스 없이도 데이터에 직접 접근할 수 있습니다. JSON 구조가 변경되더라도 코드를 크게 수정할 필요가 없습니다.
  2. 간단한 구현: 클래스 정의 없이 JSON 데이터를 쉽게 직렬화/역직렬화할 수 있습니다. 이로 인해 코드가 간결해지고 유지보수가 용이해집니다.

3. 직렬화시 var (암시적 형식 지역 변수) 사용

C#에서 var 키워드는 컴파일러가 변수의 타입을 자동으로 추론할 수 있도록 도와줍니다. var를 사용하여 익명 객체를 정의하고, Newtonsoft.Json을 사용해 JSON 문자열로 직렬화하는 예를 살펴보겠습니다.

var json = new
{
  ScreeningDataInfo = new
  {
    ShopName = "닷넷데브",
    FileName = "닷넷데브 2024-08-01 심사자료",
    EDealerRequestDate = "",
    Category = "",
    StartDate = "2023-08-01",
    EndDate = "2024-07-31"
  },
  ScreeningDataList = new[]
  {
    new
    {
      ShopID = "닷넷데브",
      MallID = "닷넷데브sub",
      MallTypeCode = "32",
      VendorCode = "KB",
      SalesReturnData = new []
      {
        new
        {
          Year = "2023",
          Month = "07",
          SalesAmount = 2185820.0,
          ReturnAmount = 88400.0,
          ReturnRate = 4.04
        },
        new
        {
          Year = "2023",
          Month = "08",
          SalesAmount = 708540.0,
          ReturnAmount = 0.0,
          ReturnRate = 0.0
        },
       ...
      }
    }
  },
  FirstExpectPayAmount = 8651050.0,
  MallTypeNameKor = "닷넷데브",
  MallType = 32,
  Total = new 
  {
    SalesAmount = 8970980.0,
    ReturnAmount = 319930.0,
    ReturnRate = 3.56
  }
};

// JSON으로 직렬화
string jsonString = JsonConvert.SerializeObject(jsonObject);
Console.WriteLine(jsonString);

이 방식은 크게 4개의 장점이 있습니다.

  1. 코드 간결화: var를 사용하면 변수 타입을 명시할 필요가 없어 코드가 간결해지고, 가독성이 높아집니다. 특히, 복잡한 타입을 가진 익명 객체를 다룰 때 유용합니다.
  2. 타입 추론: 컴파일러가 타입을 자동으로 추론해 주기 때문에, 변수를 선언할 때 일일이 타입을 지정하지 않아도 됩니다.
  3. 유연성: var를 사용하면 JSON 직렬화 시 익명 타입을 생성하고, 쉽게 JSON 문자열로 변환할 수 있습니다.
  4. 직관성: var를 사용하여 익명 객체를 생성할 때, 객체의 구조는 JSON 형식과 거의 동일하게 표현됩니다. 이는 코드가 어떻게 JSON으로 직렬화될지 쉽게 예측할 수 있게 해줍니다.

특히 유연성과 직관성을 제공한다는 점에서 저는 이방식을 선호합니다.

4. 역직렬화시 DataTable 사용

JSON 데이터를 역직렬화하여 DataTable로 변환하는 방법은, 특히 테이블 형태의 데이터를 처리할 때 매우 유용합니다. 이 방법은 데이터베이스와의 상호작용 또는 다양한 데이터 조작을 손쉽게 수행할 수 있게 해줍니다.

string jsonString = @"
{
    'ScreeningDataList': [
        {
            'ShopID': '닷넷데브',
            'MallID': '닷넷데브sub',
            'MallTypeCode': '32',
            'VendorCode': 'KB',
            'SalesReturnData': [
                {
                    'Year': '2023',
                    'Month': '07',
                    'SalesAmount': 2185820.0,
                    'ReturnAmount': 88400.0,
                    'ReturnRate': 4.04
                },
                {
                    'Year': '2023',
                    'Month': '08',
                    'SalesAmount': 708540.0,
                    'ReturnAmount': 0.0,
                    'ReturnRate': 0.0
                }
            ]
        }
    ]
}";

// JSON을 DataTable로 변환
JObject jsonObject = JObject.Parse(jsonString);
JArray salesReturnDataArray = (JArray)jsonObject["ScreeningDataList"][0]["SalesReturnData"];

DataTable dataTable = new DataTable();
dataTable.Columns.Add("Year", typeof(string));
dataTable.Columns.Add("Month", typeof(string));
dataTable.Columns.Add("SalesAmount", typeof(double));
dataTable.Columns.Add("ReturnAmount", typeof(double));
dataTable.Columns.Add("ReturnRate", typeof(double));

foreach (var item in salesReturnDataArray)
{
    DataRow row = dataTable.NewRow();
    row["Year"] = item["Year"];
    row["Month"] = item["Month"];
    row["SalesAmount"] = item["SalesAmount"];
    row["ReturnAmount"] = item["ReturnAmount"];
    row["ReturnRate"] = item["ReturnRate"];
    dataTable.Rows.Add(row);
}

// DataTable 출력
foreach (DataRow row in dataTable.Rows)
{
    Console.WriteLine($"{row["Year"]}, {row["Month"]}, {row["SalesAmount"]}, {row["ReturnAmount"]}, {row["ReturnRate"]}");
}

JSON 데이터를 DataTable로 변환하면, 구조화된 형태로 데이터를 다룰 수 있습니다. 이는 특히 대량의 데이터나 복잡한 데이터 조작이 필요한 경우에 유리합니다.

결론

C#에서 JSON을 다루기 위해 Class를 정의하는 것은 명시적이고 타입 안전한 코드를 작성할 수 있다는 장점이 있을것입니다. 그러나 점점 더 다양하고 복잡해지는 JSON 구조에 대응하기 위해선 더욱더 다양한 접근 방식을 고려해볼 필요가 있다고 생각합니다.

그래서 이번에 제가 평소에 사용하는 몇 가지 JSON 처리 방식을 소개해 보았습니다. 이러한 방법들이 특정 상황에서 유용할 수 있지만, 혹시 제가 놓친 중요한 이슈라던가 여기서 다루지 않은 다른 좋은 JSON 처리 방법도 많을거라 생각합니다.

피드백을 주시면 열심히 배워보겠습니다.

감사합니다.

5개의 좋아요

class 용도가 json 문자열에서 데이터를 읽거나 아니면, 데이터로 json 문자열을 만드는 정도의 역할이라면 귀하의 의견에 동의합니다.

그런데, class 용도가 한 발 더 나아가면 - 검색/합계 등등의 비지니스 로직 - class 만드는 것이 귀찮은 것이 아니라, 객체지향 세계가 이렇게 좋은 것이구나 라고 느낄 것입니다.

그리고, C# 에서의 코딩은 class 만드는 것 아닐까요?

3개의 좋아요

의견 김사합니다. C#이 객체지향 언어로서 Class 중심으로 생각하는건 당연하다고 생각합니다. 다만 Json을 다룰때 Class를 효율적으로 쓰는 방법을 모르겟습니다 ㅠㅠ

저도 비슷한 불편함을 느꼈지만, 결론은 약간 다릅니다.

동적 vs 정적

우선, 아래의 코드는 동적 언어의 특징입니다.

그렇기 때문에, 위 코드 이하에 있는 코드들은 컴파일러의 도움을 받을 수 없습니다.
이는 코드 작성하고 실행해 보기 전에는 오류 체크가 안된다는 의미입니다.

저게 한 두 모델이면 모르겠는데, 수십 개라면, 성격 배립니다.
물론, 인터프리팅 언어는 그렇게 하는 게 맞고 유일한 방법일 것입니다.

내부 데이터 vs 외부 데이터

내부 데이터 직렬화/역직렬화

데이터를 주고 받는 상대방이 내부 시스템 - 예를 들면, 우리가 작성한 API와 데스크탑 앱이라면, 클래스 혹은 record 를 작성하는 게 여러 모로 훨씬 효율적입니다.

한 번만 작성하면 두 프로젝트에서 직렬화/역직렬화에 모두 쓸 수 있으니까요.

외부 데이터 직렬화/역직렬화

외부 시스템 - 예를 들면, 날씨 정보 API -과 통신할 때가 사실 제일 귀찮죠.

그들이 제시한 요청 바디나 응답 바디를 클래스로 만들어야 하니까요.
그런데, 저는 아래의 사이트를 이용합니다.

Convert JSON to C# Classes Online - Json2CSharp Toolkit

VS 코드나, VS 에서 이런 기능을 하는 확장이나 플러그인을 제공해주면 더할 나위 없이 좋겠지만 아쉽게 나마 쓸 만 합니다.

참고로, 아래와 같이 무기명 객체를 생성하는 게 일견 편리해 보이지만, 엔드 포인트가 몇 십개 되는 경우라면, 클래스가 사무치게 그리워질 것입니다.

3개의 좋아요

System.Text.Json으로 Json을 1급 취급하게 되면서
JsonElement와 JsonObject를 사용하는게 좋을 거 같습니다.

dynamic의 경우 사후지원이 없는 상황이고
동적으로 취급하는 경우 런타임 에러에 매우 취약해서
프로그램이 돌아가다 죽는 경우가 급증

2개의 좋아요

비슷한 불편함에 다른 결론이 인상깊네요.
말씀하신거처럼 문제는 외부에서 들어오는 데이터를 다룰때라고 생각합니다.
JSON to C# 요거 한번 사용해봐야겠네요. 감사합니다

1개의 좋아요

System.Text.Json 요걸로 deserialze할 때 클래스를 요구해서, 자연스럽게 Newtonsoft.Json으로 넘어가게 되더라고요.

dynamic의 위험성은 다시 한번 재고해봐야겠네요. dynamic말고 Jobject로 써도 되긴합니다.
답변 감사합니다.

Instantly parse JSON in any language | quicktype

저의 경우엔 위의 quicktype을 쓰고 있습니다.

다른 이유는 없고 UI가 이뻐서 사용하고 있습니다.

마찬가지로 Visual Studio 내에서도 Json 문자열을 C# 클래스로 변환해주는 기능도 있습니다.

여기 Visual Studio Tip에 소개되고 있습니다.

저도 윗분들 의견대로 그래도 class/record를 만들어서 사용하는 것이 유지보수 측면에서 좋다고 생각합니다.

물론 예측이 불가능한 json 문자열의 경우엔 어쩔 수 없지만…그래도 최대한 패턴화를 하는 게 좋고, 요즘은 돈이 들더라도 json 문자열의 유효성 체크를 위해 OpenAI 같은 것도 이용해 볼 수 있으니 더욱 기계로 패턴화하기 좋은 시대에 살고 있는 것 같습니다.

6개의 좋아요

저번에 메뉴 둘러보다 찾았었는데 Edit -> Paste Special -> Paste JSON As Classes 사용하면 Convert JSON to C#에서 제공하는 기능이랑 비슷하게 사용할 수 있습니다.

3개의 좋아요

Vincent님이 이미 관련 내용을 올려주셨네요. :sweat_smile:

이번에 엘라스틱서치 작업을 하면서 꽤 킹받았던 부분이 이거였는데
저는 익명 형식으로 퉁쳤어요.

명명된 형식까지는 아니더라도 여기저기 주고받을 때 문제가 안 된다면
익명형식도 고려해볼만 해요.

다만 deserialize 는 그래도 명명된 클래스로 해야한다는 거…

동적 언어의 유연성을 정적언어에서 사용하는 게 쉽지 않네요… -ㅅ-

3개의 좋아요

공감합니다.
특히 Web개발에서 동적 언어의 유연성이 점점 더 요구되는 환경이라는 점은 생각해볼점인거 같습니다. 저희도 적응할 필요가 있겠지요…

이런 꿀팁이 있었네요. 잘써보겠습니다.
감사합니다

1개의 좋아요

감사합니다. :smile:

알아 보니까 꽤 괜찮네요.

이걸 왜 이제 알았을까요. ^^

How to use a JSON DOM in System.Text.Json - .NET | Microsoft Learn

read-only : JsonDocument
mutable : JsonNode

5개의 좋아요

들어온지 얼마 안됐어요 :rofl:

3개의 좋아요

단순 외부로 나가는 형태라면 익명클래스 사용하면 되는데

인바운드로 들어오는 쪽에서 답없는 구조로 주면,

진짜 클래스 짜기가 지옥같습니다… ㅠㅠ

2개의 좋아요

저는 그런 상황일때 json 문자열 보여주면서
gpt나 클러드 한테 클래스 내놓으라고 채찍질 합니다.

몇번의 주고 받고 핑퐁 대화로

말 잘 듣고 곧 잘 합니다.ㅋ

2개의 좋아요

ㅎㅎ 생각해보니 gpt대리한테 시키면 되는거였네요!

Json String을 클립보드에 복사하고
Visual Studio에서
Edit → Paste special → Paste JSON As Class 하면 클래스로 만들어줍니다.

3개의 좋아요