C#에서 개체의 깊은 복사 | CodeMaze

C#에서는 기존 개체의 복사본인 새 개체를 만들 때 얕은 복사본 또는 깊은 복사본을 만들 수 있습니다.

개체의 깊은 복사본을 생성하는 것은 원본 개체에 영향을 주지 않고 복사본을 수정해야 하거나 개체와 그 내용을 직렬화해야 할 때 필요한 경우가 많습니다. 이 글에서는 C#에서 개체의 깊은 복사를 생성하는 다양한 방법을 살펴보겠습니다.

이 문서의 소스 코드를 다운로드하려면 GitHub 리포지토리를 방문하세요.

하지만 C#에서 개체의 깊은 복사본을 만드는 방법을 살펴보기 전에 얕은 복사본과 깊은 복사본의 차이점을 이해하는 것이 중요합니다. 먼저 이에 대해 살펴보겠습니다.

얕은 복사 대 깊은 복사

얕은 복사본은 메모리에 원본 개체와 동일한 새 개체를 생성하지만 새 개체에는 여전히 원본 개체와 동일한 개체에 대한 참조가 포함되어 있습니다.

즉, 원본 개체와 복사된 개체 모두에서 참조하는 개체를 수정하면 변경 사항이 두 개체 모두에 표시됩니다.

클래스 Person로 이해해 보겠습니다.

public class Person
{
    public required string Name { get; set; }
    public required int Age { get; set; }
    public required Address Address { get; set; }
}

속성 선언에 사용되는 requried 수식어는 C# 11에서 도입되었습니다. 이 수식어는 인스턴스화 시 이 클래스의 모든 멤버가 초기화되도록 하는 데 사용됩니다.

이 클래스에는 Address 개체에 대한 참조가 포함되어 있습니다.

public class Address
{
    public required string Street { get; set; }
    public required string City { get; set; }
    public required string State { get; set; }
}

Person 개체를 만든 다음 = (할당) 연산자를 사용하여 복사하면 얕은 복사본을 만드는 것입니다.

var originalPerson = new Person
{
    Name = "Steve Doe",
    Age = 22,
    Address = new Address
    {
        Street = "123 Main St.",
        City = "Anytown",
        State = "AB"
    }
};

var copiedPerson = originalPerson;

이제 copiedPerson 변수는 원래 Person과 동일한 메모리 내 개체를 가리킵니다.

이제 copiedPerson 개체를 수정하고 그 영향을 확인해 보겠습니다.

copiedPerson.Name = "Jack Swallow";
copiedPerson.Address.Street = "456 Elmo St.";

이 경우 copiedPersonName 속성과 해당 Address 개체의 Street 속성을 수정했습니다. 반면 originalPerson 객체의 NameStreet 속성을 출력할 때는 다음과 같습니다.

Console.WriteLine($"Original Name: {originalPerson.Name}");
Console.WriteLine($"Original Street: {originalPerson.Address.Street}");

값이 변경된 것을 확인할 수 있습니다.

Original Name: Jack Swallow
Original Street: 456 Elmo St.

이것은 얕은 복사의 동작입니다.

반면에 깊은 복사는 메모리에 원본 개체와 동일한 새 개체를 생성하지만 원본 개체가 참조하는 모든 개체도 재귀적으로 복사됩니다.

이렇게 하면 복사된 개체에 대한 변경 사항이 원본 개체에 영향을 미치지 않습니다.

이제 얕은 복사본과 깊은 복사본의 차이점을 이해했으니 C#에서 개체의 깊은 복사본을 만드는 방법을 살펴봅시다.

복제 가능한 인터페이스를 통한 깊은 복사

C#에서 개체의 깊은 복사를 만드는 가장 쉬운 방법은 ICloneable 인터페이스를 구현하는 것입니다.

ICloneable 인터페이스는 원본 객체의 복사본인 새 개체를 생성하는 데 사용할 수 있는 단일 메서드인 Clone()을 정의합니다.

하지만 계속하기 전에 개체의 깊은 복사를 만들 때 ICloneable 인터페이스에 주의해야 한다는 점이 중요합니다.

ICloneable 인터페이스 구현의 주요 문제점은 Clone() 메서드를 제공하지만 이 메서드가 깊은 복사본을 생성하는지 얕은 복사본을 생성하는지 모호하다는 점입니다. 클래스마다 구현 방식이 다를 수 있습니다. 따라서 항상 깊은 복사본을 생성하기 위해 ICloneable에 의존 할 수는 없습니다.

즉, 깊은 복사를 생성하는 Clone() 메서드의 버전을 살펴 보겠습니다.

먼저 Person 클래스를 수정하여 ICloneable 인터페이스를 구현해 보겠습니다.

public class Person : ICloneable
{
    public required string Name { get; set; }
    public required int Age { get; set; }
    public required Address Address { get; set; }

    public object Clone()
    {
        var clonedPerson = new Person
        {
            Name = Name,
            Age = Age,
            Address = new Address()
            {
                Street = Address.Street,
                City = Address.City,
                State = Address.State
            }
        };

        return clonedPerson;
    }
}

여기서 Clone() 메서드는 Person 클래스의 새 인스턴스를 생성하고 원본 개체의 모든 속성을 새 개체에 복사합니다. 이 메서드는 Address 클래스의 새 인스턴스를 생성하고 모든 속성을 복사하여 재귀적으로 Address 속성을 복사합니다.

또한 코드를 더욱 리팩터링하여 Address 클래스에서도 ICloneable 인터페이스를 구현할 수 있습니다:

public class Address : ICloneable
{
    public required string Street { get; set; }
    public required string City { get; set; }
    public required string State { get; set; }

    public object Clone()
    {
        return new Address
        {
            Street = Street,
            City = City,
            State = State
        };
    }
}

이를 통해 Person 클래스의 Clone() 메서드를 수정하여 과도한 코드를 제거할 수 있습니다.

public object Clone()
{
    var clonedPerson = new Person
    {
        Name = Name,
        Age = Age,
        Address = (Address)Address.Clone()
    };

    return clonedPerson;
}

Clone() 메서드를 사용하여 새 Person 개체를 만들면 원본 개체의 깊은 복사본을 얻게 되므로 복사본에 대한 변경 사항이 원본 개체에 영향을 미치지 않습니다.

var copiedPerson = (Person)originalPerson.Clone();

이제 copiedPerson 객체의 NameStreet 속성을 수정합니다.

copiedPerson.Name = "Jack Swallow";
copiedPerson.Address.Street = "456 Elmo St.";

그리고 copiedPerson 개체의 NameStreet 속성을 출력합니다.

Console.WriteLine($"Original Name: {originalPerson.Name}");
Console.WriteLine($"Original Street: {originalPerson.Address.Street}");

원본 개체의 속성에는 변화가 없음을 알 수 있습니다.

Original Name: Steve Doe
Original Street: 123 Main St.

직렬화를 통한 깊은 복사

C#에서 개체의 깊은 복사를 만드는 또 다른 방법은 직렬화를 사용하는 것입니다.

직렬화는 개체를 파일에 저장하거나 네트워크를 통해 전송할 수 있는 바이트 스트림으로 변환하는 프로세스입니다. 역직렬화는 바이트 스트림을 다시 개체로 변환하는 프로세스입니다.

개체를 직렬화하기 전에 직렬화하려는 ro체가 직렬화 가능한지 확인하고 [Serializable]과 같은 필요한 속성으로 표시했는지 확인하는 것이 중요합니다.

[Serializable]
public class Person
{
    // Code removed for brevity
}

개체를 깊은 복사하기 위한 다양한 직렬화 기술이 있습니다. 그중 몇 가지를 좀 더 자세히 살펴보겠습니다.

XML 직렬화

XML 직렬화는 개체를 파일, 데이터베이스 또는 메모리 스트림에 저장할 수 있는 XML 형식으로 변환하는 프로세스입니다.

public static T DeepCopyXML<T>(T input)
{
    using var stream = new MemoryStream();
            
    var serializer = new XmlSerializer(typeof(T));
    serializer.Serialize(stream, input);
    stream.Position = 0;

    return (T)serializer.Deserialize(stream);
}

여기서는 직렬화된 개체를 저장하는 데 사용할 새 MemoryStream 인스턴스와 개체를 직렬화 및 역직렬화하는 데 사용할 XmlSerializer 인스턴스를 생성합니다.

Serialize() 메서드 호출은 입력 개체를 직렬화합니다. 다음으로, stream.Position을 0으로 설정하여 MemoryStream을 처음부터 읽습니다. 마지막으로 Deserialize() 메서드 호출은 MemoryStream에서 개체를 역직렬화하여 유형 T의 새 개체로 반환합니다.

이제 XML 직렬화를 사용하여 사본을 생성하는 DeepCopyXML() 메서드를 사용하여 Person 개체의 깊은 복사본을 만들 수 있습니다.

JSON 직렬화

JSON 직렬화는 개체를 네트워크를 통해 저장하고 전송할 수 있는 JSON 형식으로 변환하는 프로세스입니다.

public static T DeepCopyJSON<T>(T input)
{
    var jsonString = JsonSerializer.Serialize(input);

    return JsonSerializer.Deserialize<T>(jsonString);
}

여기서는 MemoryStream 대신 JsonSerialzer 클래스를 사용합니다.

데이터 계약 직렬화

데이터 계약 직렬화는 개체를 직렬화 및 역직렬화하는 데 사용하는 또 다른 직렬화 기법입니다. [DataContract][DataMember] 속성을 사용하여 직렬화해야 하는 개체를 표시합니다.

데이터 계약 직렬화를 사용하도록 Person 클래스를 수정해 보겠습니다.

[DataContract]
public class Person
{
    [DataMember]
    public required string Name { get; set; }

    [DataMember]
    public required int Age { get; set; }

    [DataMember]
    public required Address Address { get; set; }
}

데이터 계약 직렬화는 직렬화된 데이터를 저장할 XML 파일을 생성한다는 점에서 XML 직렬화와 유사합니다. 또한 WCF(Windows Communication Foundation) 메시지의 데이터 직렬화에도 사용됩니다.

public static T DeepCopyDataContract<T>(T input)
{
    using var stream = new MemoryStream();
            
    var serializer = new DataContractSerializer(typeof(T));
    serializer.WriteObject(stream, input);
    stream.Position = 0;
            
    return (T)serializer.ReadObject(stream);
}

구현은 XML을 사용한 직렬화와 유사합니다. 차이점은 여기서는 개체를 직렬화 및 역직렬화할 때 DataContractSerializer 인스턴스를 사용한다는 점입니다.

리플렉션을 이용한 깊은 복사

리플렉션은 런타임에 개체를 검사하고 조작할 수 있는 C#의 강력한 기능입니다.

새로운 방법으로 이 기술을 이해해 봅시다.

public static T DeepCopyReflection<T>(T input)
{
    var type = input.GetType();
    var properties = type.GetProperties();

    T clonedObj = (T)Activator.CreateInstance(type);

    foreach (var property in properties)
    {
        if (property.CanWrite)
        {
            object value = property.GetValue(input);
            if (value != null && value.GetType().IsClass && !value.GetType().FullName.StartsWith("System."))
            {
                property.SetValue(clonedObj, DeepCopyReflection(value));
            }
            else
            {
                property.SetValue(clonedObj, value);
            }
        }
    }

    return clonedObj;
}

먼저 Activator 클래스의 CreateInstance() 메서드를 사용하여 원본 개체와 동일한 유형의 새 인스턴스를 생성합니다.

그런 다음 입력 개체의 모든 속성을 반복하고 해당 값을 새로 생성된 개체에 복사합니다. 속성이 참조 유형(즉, 클래스)인 경우 메서드는 재귀적으로 자신을 호출하여 개체의 깊은 복사본을 생성합니다.

따라서 DeepCopyReflection() 메서드는 유형 T의 개체를 받아 개체의 깊은 복사본을 반환합니다.

표현식 트리를 사용한 깊은 복사

표현식 트리는 런타임에 코드를 동적으로 생성하고 컴파일할 수 있는 C#의 강력한 기능입니다.

이를 사용하여 개체의 깊은 복사를 수행하는 코드를 생성할 수 있습니다. 이 기법은 복사해야 하는 클래스를 제어할 수 없는 시나리오에서 유용할 수 있습니다.

먼저 클래스의 인스턴스를 깊은 복사하기 위해 표현식 트리를 생성하는 메서드를 만들어 보겠습니다.

private static Func<T, T> GenerateDeepCopy<T>()
{
    var inputParameter = Expression.Parameter(typeof(T), "input");

    var memberBindings = new List<MemberBinding>();
    foreach (var propertyInfo in typeof(T).GetProperties())
    {
        var propertyExpression = Expression.Property(inputParameter, propertyInfo);

        if (propertyInfo.PropertyType.IsClass && propertyInfo.PropertyType != typeof(string))
        {
            var copyMethod = typeof(DeepCopyMaker)
                .GetMethod(nameof(DeepCopyMaker.DeepCopyExpressionTrees))
                .MakeGenericMethod(propertyInfo.PropertyType);
                    
            var propertyCopyExpression = Expression.Call(copyMethod, propertyExpression);

            memberBindings.Add(Expression.Bind(propertyInfo, propertyCopyExpression));
        }
        else
        {
            memberBindings.Add(Expression.Bind(propertyInfo, propertyExpression));
        }
    }

    var memberInitExpression = Expression.MemberInit(Expression.New(typeof(T)), memberBindings);

    return Expression.Lambda<Func<T, T>>(memberInitExpression, inputParameter).Compile();
}

GenerateDeepCopy() 메서드는 지정된 유형 T의 각 속성을 반복하고 참조 유형인 각 속성에 대해 재귀를 사용하여 개체를 깊은 복사하는 표현식을 생성합니다.

그런 다음 결과 표현식 트리를 지정된 유형의 깊은 복사본 인스턴스를 생성하는 델리게이트로 컴파일합니다.

깊은 복사 델리게이트를 실행하는 다른 메서드를 만들어 보겠습니다.

public static T DeepCopyExpressionTrees<T>(T input)
{
    return GenerateDeepCopy<T>()(input);
}

DeepCopyExpressionTrees() 메서드는 지정된 개체에 대한 깊은 복사 델리게이트를 생성하고 실행하는 GenerateDeepCopy() 메서드의 간단한 래퍼입니다.

이하 원문을 확인하세요.


10개의 좋아요