언어/C#

런타임 Serialization - 1

tsyang 2022. 4. 3. 13:18

개요


serialization(직렬화) : 객체나 객체 그래프를 바이트 스트림으로 변환하는 과정

deserialization(역직렬화) : 바이트 스트림을 다시 연결된 객체 그래프로 되돌리는 과정

 

이런 직렬화와 역직렬화는 다음과 같은 기능을 한다.

  1. 응용프로그램의 상태(=객체 그래프)를 파일이나 DB에 쉽게 저장하고 다시 불러올 수 있다.
  2. 객체들을 시스템의 클립보드에 복사/붙여넣기 할 수 있다.
  3. 사용자가 객체를 수정하는 동안 원본 객체들을 백업하는 용도로 쓸 수 있다.
  4. 객체를 네트워크를 통해 전송한다.
  5. 객체를 바이트 스트림으로 메모리에 serialize하면 데이터의 암호화나 압축 같은 작업을 수행할 때 유용하다.

 

 

Serialization 해보기


public static void Main()
{
    MemoryStream stream = new MemoryStream();
    BinaryFormatter formatter = new BinaryFormatter();
    var objectGraph1 = new List<string> {"Pizza", "Chicken", "Sushi"};
    var objectGraph2 = new List<int> {32000, 18000, 15000};

    formatter.Serialize(stream, objectGraph1);
    formatter.Serialize(stream, objectGraph2);
    
    stream.Position = 0;

    //serialize 한 순서와 동일해야 함.
    var newObjectGraph1 = (List<string>) formatter.Deserialize(stream);
    var newObjectGraph2 = (List<int>) formatter.Deserialize(stream);

    foreach (var s in newObjectGraph1) Console.WriteLine(s);
    foreach (var i in newObjectGraph2) Console.WriteLine(i);
}

/*
출력
Pizza
Chicken
Sushi
32000
18000
15000
 */

 

 

  1. formatter.Serialize의 첫 번째 매개변수는 System.IO의 Stream 클래스를 상속한 타입이면 다 된다. 즉, MemoryStream외에도 FileStream, NetworkStream도 가능하다.
  2. Serialize에 넘겨주는 objectGraph는 다양한 타입을 지원하며 컬렉션도 가능하다. 만약 컬렉션 내에 개별 항목이 다른 객체를 참조한다면 그 객체들도 serialize 된다.
  3. 포맷터는 객체를 직렬화하기 위해 메타데이터와 리플렉션 기법을 사용한다.
  4. 만약 서로 다른 객체들이 동일한 객체를 참조한다면 포맷터는 이를 감지하고 각 객체를 한 번만 serialize한다.
  5. Deserialize 메서드는 객체 그래프의 최상위 루트 객체를 반환한다.
  6. Serialize와 Deserialize를 할 때는 동일한 타입의 포맷터를 사용해야 한다.
  7. 위와 같은 방법을 객체를 Deep Copy하는데에 사용할 수 있다.
FCL은 BinaryFormatter라는 포맷터 이외에도 SoapFormatter라는 포맷터도 내장하고 있다. SoapFormatter는 XML 텍스트로 객체를 Serialize 해주기 때문에 디버깅 용도로 유용하지만, obsolete로 지정되었으므로 (3.5부터) 실 제품 코드 개발시 XML로 (역)직렬화가 필요하다면 XmlSerializer나 DataContractSerializer 등을 사용하는게 좋다.

 

어셈블리와의 연관성

객체를 serialize하면 스트림에 타입의 전체 이름과 타입을 정의하는 어셈블리의 이름을 같이 저장한다. 포맷터가 객체를 deserialize할 때는 이를 이용하여 현재 수행 중인 앱도메인에 어셈블리를 로드한다.

 

 

 

Serialize 가능한 타입 정의하기


기본적으로 새로 작성하는 타입은 serialize가 안 된다! 

 

따라서 serialize가 되게 만들어 줘야 하는데 아주~ 간단하다.

 

[System.Serializable]
public struct Point
{
    public int x, y;
}

그냥 이렇게 [Serializable]이란 어트리뷰트를 달아주면 된다. 대신 객체 내의 타입들이 모두 serialize 가능한 타입이어야 한다. 아니라면 익셉션이 발생한다.

 

객체를 serialize 하는 도중 예외가 발생하면 스트림에 손상된 데이터가 들어간다. (중간에 예외를 handle 했다 하더라도) 따라서 이를 방지하려면 미리 serialize 하려는 객체들을 MemoryStream에 serialize 하여 테스트 해볼 수 있다.

 

 

Serialization과 Deserialzation 제어하기


[Serializable] 어트리뷰트를 달면 private, protected를 포함한 모든 인스턴스가 serialize된다. 그러나 일부 필드를 serialize 대상에서 제외할 필요가 있을 때가 있는데 다음과 같다.

 

  1. deserialize 했을 때 의미 없는 필드인 경우. (예 : 파일, 스레드, 뮤텍스, 세마포어, 이벤트..)
  2. 필드가 가지고 있는 정보가 쉽계 계산 가능한 정보일 경우. 

이런 경우 간단하게 [NonSerialized] 어트리뷰트를 달아주면 된다. 다만 이렇게하면 deserialize로 객체가 생성될 때 해당 필드가 초기화 되지 않을 것이므로 추가적인 작업이 필요 할 수 있다. 다음의 예를 보자.

 

[Serializable]
internal class Circle
{
    private double _radius;

    [NonSerialized]
    private double _area;

    public double Area => _area;

    public Circle(double radius)
    {
        _radius = radius;
        _area = radius * radius * Math.PI;
    }
}

Circle 클래스에서 넓이는 반지름을 가지고 계산할 수 있으므로 굳이 serialize 하지 않아도 된다. 따라서 _area 필드에 [NonSerialized] 어트리뷰트를 추가하여 serialize 대상에서 제외한다. 

 

그러나 이러면 다음과 같은 문제가 발생한다.

public static void Main()
{
    Circle c = new Circle(10);

    var stream = new MemoryStream();
    var formatter = new BinaryFormatter();

    formatter.Serialize(stream, c);
    stream.Position = 0;

    var newCircle = (Circle)formatter.Deserialize(stream);
    Console.WriteLine(newCircle.Area);
}

/*
출력
0
 */

deserialize 하여 생성된 객체인 newCircle의 _area 필드가 초기화 되지 않아 값이 0이다.

 

따라서 다음과 같은 메서드를 추가해준다.

[Serializable]
internal class Circle
{
    // .. 이전 코드와 동일

    [System.Runtime.Serialization.OnDeserialized]
    private void OnDeserialized(StreamingContext context)   //아무 이름이나 ㄱㅊ
    {
        _area = Math.PI * _radius * _radius;
    }
}

 

Deserialize를 수행할 때 포맷터는 [OnDeserialized] 어트리뷰트가 부여된 메서드가 있는지 확인하고 이를 호출해준다. (따라서 메서드 이름은 상관없음.) 

 

이 외에도 같은 네임스페이스에 [OnDeserializing], [OnSerializing], [OnSerialized] 특성이 더 있으며 [OnDeserialized] 와 [OnSerialized]는 (de)serialize 에 호출되며, [OnSerializing], [OnDeserializing] 은 (de)serialize 에 호출된다.

 

이러한 메서드들은 private로 선언하는 것이 좋다. 어쩌피 포맷터는 리플렉션으로 호출할 수 있기 때문이다. 

 

만약 여러 객체를 serialize한다면 모든 객체들의 [OnSerializing] 메서드를 수행한 후에 객체들을 serialize 하고 다시 모든 객체들의 [OnSerialized] 메서드를 호출한다. deserialize도 마찬가지이다. 이렇게 되면 다른 객체를 참조하는 상황에서도 잘 동작하게 된다.

 

[OptionalField]

만약 어떤 타입이 serialize 되어 파일로 저장된 뒤 해당 타입에 새로운 필드가 추가되었다고 가정하자. 이후 deserialize를 수행하면 예외가 발생한다. 이를 방지하기 위해 [OptionalField] 어트리뷰트를 달아주면 포맷터는 해당 필드가 스트림에 존재하지 않더라도 예외를 발생시키지 않는다.

[Serializable]
internal class Circle
{
	//이전과 동일

    [System.Runtime.Serialization.OptionalField]
    private Color _color;
}

 

 

주의

[Serializable]
internal class Circle
{
    public double Radius { get; private set; }
}

위와 같이 C#의 자동 속성 기능을 serialize 대상에 포함하면 안 된다. 자동 속성 기능에 의해 생성되는 필드의 이름은 컴파일마다 바뀔 수 있기 때문이다.

 


참고 : 제프리 리처, CLR via C# 4판 , 비제이퍼블릭, 24장. 런타임 serialization [713~725p]

'언어 > C#' 카테고리의 다른 글

CLR 스레딩2 단순 계산 작업  (1) 2022.04.17
CLR 스레딩1 기본  (0) 2022.04.10
C# - 리플렉션 (Reflection)  (0) 2021.07.04
C# - CLR 호스팅과 앱도메인  (1) 2021.06.27
C# - 관리 힙과 GC (2)  (1) 2021.04.03