객체 복사

MemberwiseClone을 활용한 얕은 복사

MemberwiseClone은 객체의 얕은 복사Shallow Copy를 수행합니다. 이는 클래스의 필드값만 복사하며, 참조 타입 필드는 원본 객체의 참조를 복사합니다.

  • 객체의 모든 필드를 그대로 복사.
  • 값 타입primitive types 필드는 값으로 복사.
  • 참조 타입reference types은 참조로 복사.
  • String은 참조 타입이지만 불변성 보장.
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; } // 참조 타입 필드
    public Person Copy() => (Person)this.MemberwiseClone();
}
public class Address
{
    public string Street { get; set; }
}
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123" }
};
var person2 = (Person)person1.MemberwiseClone();
person2.Name = "Kim"
person2.Address.Street = "456";
Debug.WriteLine(person1.Name); // 출력 : John
Debug.WriteLine(person2.Name); // 출력 : Kim
Debug.WriteLine(person1.Address.Street); // 출력 : 456
Debug.WriteLine(person2.Address.Street); // 출력 : 456

장점

  • 구현이 간단하고 빠름.
  • 값 타입만 있는 경우 충분히 사용할 수 있음.

단점

  • 참조 타입 필드를 복사하지 않음(얕은 복사).
  • 내부 필드가 참조 타입일 경우, 원본 객체와 복사된 객체가 같은 데이터를 공유.

ICloneable 인터페이스

ICloneable을 구현하여 복사 로직을 커스터마이징할 수 있습니다. 참조 타입 필드를 깊은 복사하려면, 직접 복사 논리를 구현해야 합니다.

  • Clone 메서드를 오버라이드하여 원하는 복사 방식을 구현.
  • 깊은 복사를 위해 각 참조 필드를 개별적으로 복사.
public class Person : ICloneable
{
    public string Name { get; set; }
    public Address Address { get; set; }
    public object Clone()
    {
        return new Person
        {
            Name = this.Name,
            Address = new Address { Street = this.Address.Street }
        };
    }
}
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123" }
};
var person2 = (Person)person1.Clone();
person2.Address.Street = "456";
Console.WriteLine(person1.Address.Street); // 출력: 123 Main St (독립된 객체)

장점

  • 깊은 복사를 커스터마이징 가능.
  • 코드가 간결하며 컨트롤 가능.

단점

  • 구현이 번거롭고, 클래스가 중첩될수록 복잡해짐.
  • 모든 클래스에 Clone 구현 필요.

JSON 직렬화

JSON 직렬화 및 역직렬화를 사용하면 객체의 모든 데이터를 새로운 객체로 복사할 수 있습니다. 참조 타입 필드도 완전히 독립적으로 복사됩니다.

  • 객체를 JSON 문자열로 직렬화.
  • JSON 문자열을 다시 객체로 역직렬화.
using System.Text.Json;
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}
public class Address
{
    public string Street { get; set; }
}
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123" }
};
// 깊은 복사
var json = JsonSerializer.Serialize(person1);
var person2 = JsonSerializer.Deserialize<Person>(json);
person2.Name = "Kim";
person2.Address.Street = "456";
Debug.WriteLine(person1.Name); // 출력 : John
Debug.WriteLine(person2.Name); // 출력 : Kim
Debug.WriteLine(person1.Address.Street); // 출력 : 456
Debug.WriteLine(person2.Address.Street); // 출력 : 456

장점

  • 모든 데이터가 깊은 복사됨.
  • 참조 타입 복사가 자동으로 이루어짐.

단점

  • 성능이 상대적으로 느림.
  • JSON 직렬화가 지원되지 않는 타입 처리 필요(예: Stream).

XML 직렬화

System.Xml.Serialization을 활용한 XML 직렬화/역직렬화를 통해 깊은 복사를 구현할 수 있습니다. XML은 객체를 계층적으로 표현할 수 있어 데이터 교환이나 저장에 적합합니다.

  • 객체를 XML로 직렬화.
  • XML 데이터를 다시 객체로 역직렬화.
  • 참조 타입도 독립적으로 복사되므로 깊은 복사가 가능합니다.
using System;
using System.IO;
using System.Xml.Serialization;
[Serializable]
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}
[Serializable]
public class Address
{
    public string Street { get; set; }
}
public class Program
{
    public static T DeepCopy<T>(T source)
    {
        var serializer = new XmlSerializer(typeof(T));
        using (var stream = new MemoryStream())
        {
            // 직렬화
            serializer.Serialize(stream, source);
            stream.Position = 0;
            // 역직렬화
            return (T)serializer.Deserialize(stream);
        }
    }
    static void Main()
    {
        var person1 = new Person
        {
            Name = "John",
            Address = new Address { Street = "123" }
        };
        var person2 = DeepCopy(person1);
        person2.Address.Street = "456";
        Console.WriteLine(person1.Address.Street); // 출력: 123
    }
}

장점

  • XML을 사용하여 직렬화된 데이터를 사람이 읽기 쉽고, 외부 시스템과 호환 가능.
  • 객체 계층 구조를 쉽게 표현 가능.

단점

  • 성능이 JSON이나 바이너리 직렬화보다 느림.
  • XML 태그로 인해 직렬화된 데이터 크기가 커짐.

Reflection을 활용한 깊은 복사

리플렉션을 사용하여 객체의 모든 필드를 자동으로 복사합니다.

  • 객체의 필드를 탐색하여 값 타입과 참조 타입을 구분.
  • 참조 타입은 재귀적으로 복사.
using System.Reflection;
public static class ObjectExtensions
{
    public static T DeepCopy<T>(this T source)
    {
        if (source == null) return default;
        var type = source.GetType();
        var instance = Activator.CreateInstance(type);
        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
        {
            var value = field.GetValue(source);
            field.SetValue(instance, value is ICloneable cloneable ? cloneable.Clone() : value);
        }
        return (T)instance;
    }
}
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123 Main St" }
};
var person2 = person1.DeepCopy();
person2.Address.Street = "456 Park Ave";
Console.WriteLine(person1.Address.Street); // 출력: 123 Main St (독립된 객체)

장점

  • 모든 객체를 다룰 수 있음.
  • 커스터마이징 가능.

단점

  • 성능이 느림(리플렉션 사용).
  • 특정 필드에서 예외가 발생할 수 있음.

Protocol Buffers 직렬화

Protocol Buffers는 Google에서 개발한 직렬화 프레임워크로, 크로스 플랫폼 및 성능에 최적화된 직렬화/역직렬화 방식을 제공합니다.

  • 객체를 Protobuf 형식으로 직렬화.
  • 직렬화된 데이터를 Protobuf 메시지를 통해 다시 역직렬화. 먼저, .proto 파일을 정의합니다:
syntax = "proto3";
message Person {
  string name = 1;
  Address address = 2;
}
message Address {
  string street = 1;
}

다음으로, C#으로 컴파일된 클래스를 사용합니다:

using Google.Protobuf;
using System.IO;
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123" }
};
// 직렬화
using (var stream = new MemoryStream())
{
    person1.WriteTo(stream);
    stream.Position = 0;
    // 역직렬화
    var person2 = Person.Parser.ParseFrom(stream);
    
    person2.Address.Street = "456";
    Console.WriteLine(person1.Address.Street); // 출력: 123
}

장점

  • 성능이 매우 빠르고 데이터 크기가 작음.
  • 네트워크 통신, 크로스 플랫폼 시스템에 적합.

단점

  • 추가 설정과 .proto 파일 작성 필요.
  • XML이나 JSON처럼 사람이 읽기 쉬운 형식이 아님.

Expression Trees를 사용한 복사

리플렉션과 비슷한 방식이지만, Expression Trees를 사용하면 복사 코드를 동적으로 생성하여 성능을 최적화할 수 있습니다.

  • 객체의 필드를 동적으로 탐색하여, 복사 코드를 생성.
  • 리플렉션보다 빠르고, 더 나은 성능을 제공.
using System;
using System.Linq.Expressions;
public static class ObjectExtensions
{
    public static T DeepCopy<T>(T source)
    {
        var parameter = Expression.Parameter(typeof(T), "source");
        var memberBindings = typeof(T).GetProperties()
            .Select(prop => Expression.Bind(
                prop, Expression.Property(parameter, prop)));
        var body = Expression.MemberInit(
            Expression.New(typeof(T)), memberBindings);
        var lambda = Expression.Lambda<Func<T, T>>(body, parameter);
        var compiled = lambda.Compile();
        return compiled(source);
    }
}
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}
public class Address
{
    public string Street { get; set; }
}
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123" }
};
var person2 = ObjectExtensions.DeepCopy(person1);
person2.Address.Street = "456";
Console.WriteLine(person1.Address.Street); // 출력: 123

장점

  • 리플렉션보다 빠름.
  • 런타임 시 코드 생성을 통해 효율적.

단점

  • 구현이 복잡하고, 깊은 복사가 아닌 경우 추가 로직 필요.

AutoMapper를 사용한 깊은 복사

AutoMapper는 객체 매핑 라이브러리로, 깊은 복사를 위해 사용할 수 있습니다.

  • AutoMapper를 설정하여, 소스 객체와 대상 객체를 매핑.
  • 객체 구조에 따라 깊은 복사 수행.
using AutoMapper;
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}
public class Address
{
    public string Street { get; set; }
}
var config = new MapperConfiguration(cfg =>
{
    cfg.CreateMap<Person, Person>();
    cfg.CreateMap<Address, Address>();
});
var mapper = config.CreateMapper();
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123" }
};
var person2 = mapper.Map<Person>(person1);
person2.Address.Street = "456";
Console.WriteLine(person1.Address.Street); // 출력: 123

장점

  • 설정이 간단하고 사용이 쉬움.
  • 복잡한 객체 구조도 쉽게 매핑 가능.

단점

  • 라이브러리 의존성.
  • 특정 케이스에서 깊은 복사 설정이 필요.

BinaryFormatter (이진 직렬화)

.NET Framework에서 제공되는 BinaryFormatter를 사용하여 객체를 직렬화하고 역직렬화하여 깊은 복사를 수행합니다.

  • 객체를 이진 데이터로 직렬화.
  • 이진 데이터를 다시 객체로 역직렬화.
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
[Serializable]
public class Person
{
    public string Name { get; set; }
    public Address Address { get; set; }
}
[Serializable]
public class Address
{
    public string Street { get; set; }
}
var person1 = new Person
{
    Name = "John",
    Address = new Address { Street = "123" }
};
var formatter = new BinaryFormatter();
using (var stream = new MemoryStream())
{
    formatter.Serialize(stream, person1);
    stream.Seek(0, SeekOrigin.Begin);
    var person2 = (Person)formatter.Deserialize(stream);
    person2.Address.Street = "456";
    Console.WriteLine(person1.Address.Street); // 출력: 123
}

장점

  • JSON 직렬화와 유사하지만 이진 데이터로 처리하여 더 효율적.
  • 모든 필드 깊은 복사 가능.

단점

  • [Serializable] 속성을 추가해야 함.
  • .NET Core 및 .NET 5 이상에서는 권장되지 않음(보안 문제).

종합 비교

방법장점단점
MemberwiseClone간단하고 빠름참조 타입 필드가 얕은 복사됨.
ICloneable커스터마이징 가능모든 클래스에 Clone 구현 필요.
JSON 직렬화참조 타입 포함, 모든 데이터 깊은 복사성능이 느리고 특정 타입 제한 있음.
XML 직렬화사람이 읽을 수 있고, 계층적 표현 가능성능이 느리고, 태그로 인해 데이터 크기 큼
Reflection모든 객체 처리 가능느리고 복잡.
Protocol Buffers빠르고 데이터 크기가 작음설정이 복잡하고 사람이 읽기 어려움
Expression Trees리플렉션보다 빠르고 효율적구현이 복잡
AutoMapper설정 간단, 복잡한 객체 구조 매핑 가능외부 라이브러리 의존
BinaryFormatter모든 필드 깊은 복사, 이진 데이터 처리보안 문제 및 .NET Core에서 비권장.