.NET/C#

[C#] File 다루기 - 4. 객체의 직렬화(Serialization)

클리엘 2022. 6. 26. 06:32
728x90

4. 객체의 직렬화(Serialization)

 

직렬화는 객체를 특정한 형식에 맞춰 byte배열로 변환하여 처리하는 것을 말합니다. 역직렬화는 반대의 처리 개념이며 이는 객체의 현재 상태를 그대로 저장해 필요할 때 다시 객체를 그대로 재현할 수 있도록 합니다. 마치 게임에서 현재 상태를 저장해 뒀다가 다음날 마지막에 했던 플레이 상태를 그대로 되돌려 계속해서 게임을 진행하는 것과 같습니다. 직렬화된 객체는 파일이나 Database로 그 자체를 데이터화하여 저장할 수 있기 때문입니다.

 

객체를 직렬 화하는 데 사용되는 몇 가지 형식이 존재하지만 오늘날 가장 많이 사용하는 방식은 XML과 JSON 이 2가지입니다. JSON은 Web이나 Mobile과 같이 제한된 환경하에서 가장 최적의 선택이 될 수 있으며 XML은 JSON보다 풍부한 표현방식을 제공할 수 있지만 비교적 다양한 처리방식과 대역폭에 여유가 있는 시스템에서 사용하는 것이 좋습니다.

 

.NET은 XML과 JSON의 직렬화를 위한 여러 클래스를 가지고 있지만 대부분은 XmlSerializer와 JsonSerializer를 사용하는 것으로 충분할 것입니다.

 

(1) XML 다루기

 

우선 간단히 아래와 같은 Class를 생성합니다. 이 Class는 1개의 매개변수를 갖는 생성자를 갖고 있으며 멤버 중 Speed는 Protected로 정의되어 있습니다.

public class Car
{
    public Car() {

    }
    
    public Car(int speed)
    {
        Speed = speed;
    }

    public string? Name { get; set; }
    protected int Speed { get; set; }
}

참고로 아무런 매개변수도 없는 기본 생성자를 포함하고 있는데 이는 역직렬화를 수행할 때 Serializer가 Car의 인스턴스화를 위해 기본 생성자를 호출하게 되므로 명시적으로 해당 생성자가 존재해야 합니다.

 

클래스를 만들고 나면 위 객체를 Serializing 하는 아래 코드를 작성합니다.

using System.Xml.Serialization;

using static System.Environment; 
using static System.IO.Path;

List<Car> buses = new() { new(60) { Name = "100번버스" }, new(80) { Name = "200번버스" } };

string savePath = Combine(CurrentDirectory, "buses.xml");

XmlSerializer xs = new(buses.GetType());
using (FileStream stream = File.Create(savePath))
{
    xs.Serialize(stream, buses);
}

이 예제는 Car Type의 List를 생성하고 해당 객체를 그대로 buses.xml이라는 파일로 직렬화를 수행하도록 합니다. 위 코드를 실행하면 아래와 같은 XML을 생성하게 되는데

여기서 Speed는 public이 아니므로 포함되지 않았습니다.

 

이번에는 Car Type을 수정해 Speed를 public으로 바꾸고 아래와 같이 'XmlAttribute'이라는 Annotation을 Decorate 합니다. XML직렬화에서 포함되는 건 오로지 public의 Field와 Property 뿐입니다.

[XmlAttribute("속도")]
public int Speed { get; set; }

그리고 다시 Code를 실행하면 이번에는 아래와 같은 형태의 XML이 생성됨을 확인할 수 있습니다.

보시는 바와 같이 XmlAttribute는 XML에서 Attribute로 적용됩니다. XML의 요소가 많이 생성되면 그만큼 XML의 덩치가 커지는데 일부분은 이와 같은 방법으로 Attribute로 처리해 두면 그 만큼 용량을 줄일 수 있게 됩니다.

 

이 외에도 많은 Annotation이 있으며 반대로 어떠한 Annotation도 사용하지 않으면 XmlSerializer는 역직렬화를 수행할 때 객체의 속성을 사용하고 대소문자를 구별하지 않습니다.

 

● XML의 역직렬화

 

직렬화된 XML을 역직렬화 하려면 XmlSerializer의 Deserialize() 메서드로 읽고자 하는 XML 파일을 지정하면 됩니다.

using System.Xml.Serialization;

using static System.Environment; 
using static System.IO.Path;

string savePath = Combine(CurrentDirectory, "buses.xml");

List<Car> buses = new();
XmlSerializer xs = new(buses.GetType());

using (FileStream xmlLoad = File.Open(savePath, FileMode.Open))
{
    List<Car>? loadBuses = xs.Deserialize(xmlLoad) as List<Car>;
    
    if (loadBuses is not null)
    {
        foreach (Car b in loadBuses)
        {
            Console.WriteLine($"{b.Name}의 속도 : {b.Speed}");
        }
    }
}
// 100번버스의 속도 : 60
// 200번버스의 속도 : 80

(2) JSON 다루기

 

.NET에서 JSON으로의 직렬화에 가장 많이 사용되는 것으로 Newtonsoft.Json이 있습니다. Json.NET으로도 알려져 있으며 Json을 다뤄야 하는 상황에서 강력한 성능과 기능을 제공하고 있습니다.

 

Newtonsoft.Json을 사용하려면 우선 프로젝트 파일(csproj)에서 아래와 같이 Newtonsoft.Json패키지를 추가해야 합니다.

<ItemGroup>
  <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

그리고 XML에서 사용했던 방법과 비슷하게 Nettonsoft.Json을 통해 Car객체의 List를 직렬 화합니다.

using static System.Environment; 
using static System.IO.Path;

List<Car> buses = new() { new(60) { Name = "100번버스" }, new(80) { Name = "200번버스" } };

string savePath = Combine(CurrentDirectory, "buses.json");

using (StreamWriter jsonStream = File.CreateText(savePath))
{
    Newtonsoft.Json.JsonSerializer js = new();
    js.Serialize(jsonStream, buses);
}

Console.WriteLine(File.ReadAllText(savePath));
//[{"Name":"100번버스","Speed":60},{"Name":"200번버스","Speed":80}]

● JSON의 역직렬화

 

.NET Core 3.0에서는 Json과 관련한 새로운 Namespace인 System.Text.Json을 추가하였는데 이것은 Span<T>와 같은 API를 활용하여 성능을 최적화시킨 것입니다.

 

이전 Json.NET과 같은 라이브러리는 UTF-16을 읽음으로써 구현되는데 HTTP를 포함한 대부분의 네트워크 프로토콜이 UTF-8을 사용하고 Json의 Unicode 문자열 값이 UTF-8로 트랜스 코딩되는 것을 피할 수 있기 때문에 UTF-8을 사용하는 JSON문서를 읽고 쓰는데 더 성능이 좋을 수 있습니다.

 

새로운 API를 통해서 마이크로소프트는 경우에 따라 1.3에서 5배까지의 성능 향상을 이뤘는데 Json.NET과 새로운 API가 여전히 공존하는 상황에서 본래 Json.NET개발자이자 마이크로소프트와 함께 새로운 Json Type을 개발한 James Newton-King은 Json.NET에 대해서 'Json.NET은 사라지지 않았다.'라고 언급하였으며 '간소함과 고성능이 필요한 경우에 대한 다른 선택'이라고 밝힌 바 있습니다.

 

아래 예제는 새로운 JSON API를 사용한 역직렬화의 방법을 보여주고 있습니다.

using NewJson = System.Text.Json.JsonSerializer;

using static System.Environment; 
using static System.IO.Path;

string savePath = Combine(CurrentDirectory, "buses.json");

using (FileStream json = File.Open(savePath, FileMode.Open))
{
    List<Car>? buses = await NewJson.DeserializeAsync(utf8Json: json, returnType: typeof(List<Car>)) as List<Car>;
    
    if (buses is not null)
    {
        foreach (Car bus in buses)
        {
            Console.WriteLine($"{bus.Name}번 버스의 속도는 {bus.Speed}입니다.");
        }
    }
}
// 100번버스번 버스의 속도는 60입니다.
// 200번버스번 버스의 속도는 80입니다.

개인적으로 개발의 생산성이나 기능적인 면을 위해서는 Json.NET을, 성능이 필요한 경우에서는 System.Text.Json 사용하는 것으로 선택의 기준을 잡을 것을 권장합니다.

 

5. JSON 제어

 

JSON을 다루는 데 있어서 적용 가능한 아래와 같은 여러 옵션이 존재합니다.

  • Field의 포함과 제외
  • Member의 이름 적용 정책
  • 대소문자 구분 정책
  • 압축과 공백을 사용한 포맷유지 간의 선택

해당 옵션들은 JsonSerializerOptions를 통해 아래와 같이 지정할 수 있습니다.

using System.Text.Json;
using System.Text.Json.Serialization;

using static System.Environment; 
using static System.IO.Path;

Car bus = new(60) { Name = "100번버스", Driver = "홍길동", RPM = 2000 };

JsonSerializerOptions options = new() { IncludeFields = true, PropertyNameCaseInsensitive = true, WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase };

string savePath = Combine(CurrentDirectory, "bus.json");

using (Stream fileStream = File.Create(savePath))
{
    JsonSerializer.Serialize<Car>(utf8Json: fileStream, value: bus, options);
}

Console.WriteLine(File.ReadAllText(savePath));
// {
//   "name": "100\uBC88\uBC84\uC2A4",
//   "speed": 60,
//   "driver": "\uD64D\uAE38\uB3D9",
//   "rpm": 2000
// }

JsonSerializerOptions에서 IncludeFields는 해당 객체의 모든 Field를 직렬화 대상으로 할 것인지 여부를 결정합니다. 만약 이 설정이 false라면 객체에서 [JsonInclude]로 지정된 Field만 직렬화 대상이 됩니다.

 

PropertyNameCaseInsensitive는 역직렬화시 대소문자를 구별하지 않는 방법으로 비교할 것인지를 지정합니다. 이 설정이 false면 역직렬화에 대한 성능 향상을 기대해 볼 수 있습니다.

 

WriteIndented는 가독성을 위해 JSON에 들여 쓰기를 적용할지를 지정합니다. 이 설정이 false면 가독성을 떨어지지만 JSON문서의 크기는 줄일 수 있게 됩니다.

 

PropertyNamingPolicy는 JsonNamingPolicy.CamelCase로 지정되었는데 이는 Member의 이름을 소문자로 시작하는 CamelCase로 적용한다는 것을 의미합니다. 따라서 멤버의 이름이 DriverName로 되어 있다 하더라도 직렬화에서는 driverName으로 적용될 것이며 이는 Javascript를 다루게 되는 browser환경에서 적합한 방식입니다.

 

● Newtonsoft에서 새로운 JSON으로의 마이그레이션

 

기존에 Newtonsoft Json.NET을 활용한 Code를 새로운 System.Text.Json네임스페이스로 마이그레이션 하고자 하는 경우 아래 링크를 참조하면 마이그레이션 단계에서 필요한 사항을 찾을 수 있을 것입니다.

 

Migrate from Newtonsoft.Json to System.Text.Json - .NET | Microsoft Docs

 

Migrate from Newtonsoft.Json to System.Text.Json - .NET

Learn how to migrate from Newtonsoft.Json to System.Text.Json. Includes sample code.

docs.microsoft.com

 

728x90