객체 복사
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에서 비권장. |