티스토리 뷰

Others

[C#] Object Clone

해구름 2023. 5. 24. 11:16
반응형

C#에서 Object를 복제하는 것은 쉽고 단순해 보이지만 실제로는 복잡하고 실수가 발생하기 쉬운 작업 입니다. 여기서는 C# Object 복제에 관한 다양한 방법들을 소개합니다.

얕은복사 vs 깊은복사

객체를 복사하는 방법은 크게 얕은복사(Shallow Cloning)와 깊은복사(Deep Cloning) 2가지로 나뉩니다.

구분 값 유형
(Value Type)
참조 유형
(Reference Type)
얕은복사 값 복사 참조된 객체 공유
깊은복사 값 복사 참조된 객체 복사

두 방식 모두 값 유형(Value Type)의 멤버변수에 대해서는 동일하게 값 복사를 진행합니다. 참조 유형(Reference Type)의 멤버변수에 대해, 얕은복사는 주소 값을 공유하며 깊은복사는 객체를 복사합니다. 그림으로 나타내면 다음과 같습니다.

얕은복사 (Shallow Cloning) 깊은복사 (Deep Cloning) 원본객체 복제된 객체 원본객체 복제된 객체 참조된 객체 참조된 객체 (원본) 참조된 객체 (복제됨)

깊은복사와 얕은복사에 관한 자세한 사항은 Wikipedia를 참조해주세요.

ICloneable Interface

.NET에서는 복제를 위해 IClonable Interface를 제공합니다. ICloneable Interface는 Clone 메서드를 포함하고 있습니다. 개발자는 객체를 복제하여 반환하도록 Clone 메서드를 구현해야합니다.

public interface ICloneable
{
    object Clone();
}

ICloneable은 한가지 문제가 있습니다. Clone 메서드가 깊은복사를 수행해야하는지 얕은복사를 수행해야하는지 구체적으로 명시되어 있지 않기 때문에 Clone 메서드를 호출하는 개발자들은 깊은복사가 수행되는지 아니면 얕은복사가 수행되는지 알 수가 없습니다. MSDN 문서를 살펴보면 Clone 메서드는 깊은복사를 수행한다는 간적적인 암시가 있지만 명확하게 결정된 사항은 아닙니다.

"ICloneable Interface는 Clone 멤버 함수를 포함하고 있습니다. Clone 함수는 MemberwiseClone 메서드의 복제 기능을 넘어서기 위한 목적으로 제공됩니다... MemberwiseClone 메서드는 얕은 복사를 수행합니다..."

2023년 5월 MSND 문서을 살펴보면 Clone 메서드는 얕은복사, 깊은복사 모두 제공할 수 있다고 소개하고 있습니다. 다만 Clone 메서드를 호출하는 개발자는 어떤 복사가 일어나는지 알 수 없기 때문에, 공용API를 개발하는 경우에는 Clone 메서드 제공을 권장하지 않고 있습니다. 이러한 모호함을 해결하기 위해 IClonable을 사용하는 대신 직접 Interface를 선언하여 사용하기도 합니다.

public interface ICopyable
{
    object Copy(); //ShallowCopy
    object DeepCopy();
}

Type-Safe Clone

Clone 메서드는 Object 타입을 반환합니다. Clone 메서드를 호출하는 개발자는 항상 적절한 타입으로 형변환을 수행해야합니다. 매번 형변환을 수행하는 것도 번거로운 일이지만 개발자가 형변환을 수행할 때 실수를 하기도 합니다. 컴파일러는 이러한 형변환 오류를 잡아내지 못하기에 런타임 오류로 이어질 수 있습니다.

Person person = new Person();
person.Name = "홍길동";
Person personClone = (Person)person.Clone();

문제를 해결하려면 다음과 같이 IClonable를 구현함과 동시에 타입에 안전한(Type-Safe) Clone 메서드를 추가 정의하면 됩니다.

public class Person : ICloneable
{
    public string Name;
    object ICloneable.Clone()
    {
        return this.Clone();
    }
    //Type-safe Clone 메서드 제공
    public Person Clone()
    {
        return (Person)this.MemberwiseClone();
    }
}

다양한 Clone 방법

객체 복제에 있어 표준화된 정답은 없으며 목적에 따라 적절한 방법을 선택하면 됩니다. 호출 목적은 무엇인지, 호출 빈도는 얼마나 되는지, 성능을 중시해야하는지, 반드시 깊은 복제를 수행해야하는지, 깊은 복제를 수행한다면 몇단계까지 복제할 것인지, 순환참조는 없는지, private 접근한정자로 복제하기 어려운 경우는 없는지 등 다양한 부분을 검토해야 합니다.

여기에서는 객체 복제에 관한 몇가지 방법들을 소개합니다.

1. 직접 복사하기

가장 빠르고 단순한 방법입니다. 모든 멤버변수들을 직접 복사하는 것입니다. 개발자가 의도한 대로 동작하도록 보장할 수 있으며 유연성이 가장 높은 방법입니다.

public class Person : ICloneable
{
    public string Name;
    public int Age;
    ...

    public object Clone()
    {
        Person personClone = new Person();
        personClone.Name = this.Name;
        personClone.Age = this.Age;
        ...

        return personClone;
    }
}

이 방법의 단점은 지루하고 반복적인 코딩이라는 점입니다. 개발자가 코딩 중에 실수할 확률도 높습니다. 유지보수하는 과정에서 특정 멤버변수의 복사를 누락하는 경우도 있습니다. 이를 해결하기 위해 코드를 자동 생성해주는 Code Generator 도구를 사용하기도 합니다. 컴파일 할 때마다 Clone 메서드 코드가 자동 생성되므로 단점을 극복할 수 있습니다. .NET에서는 Source Generator 기능을 소개하고 있으며 이와 관련된 패키지도 찾아볼 수 있습니다.

2. MemberwiseClone 메서드 사용

Object 객체에서 기본적으로 제공하는 MemberwiseClone 메서드를 사용하면 간단히 얕은 복사를 구현 할 수 있습니다. 값타입(Value Type) 멤버변수에 대해서는 비트단위로 복사(bit-by-bit copy)가 진행되며, 참조타입(Reference Type) 멤버변수에 대해서는 참조하는 인스턴스의 주소 값만 복제됩니다. MemberwiseClone 메서드는 상속구조의 클래스에도 정상적으로 동작하기 때문에, 가장 최상위 Base Class에 한번만 정의하면 자식 클래스에는 재정의하지 않아도 됩니다.

public class Person : ICloneable
{
    public string Name;
    public int Age;
    ...

    public object Clone()
    {
        return this.MemberwiseClone();
    }
}

public class Student : Person
{
    public string TeacherName;
    public string Course;
    
    //부모의 Clone 메서드를 상속하므로 구현할 필요 없음
    //public object Clone()
    //{
    //    return this.MemberwiseClone();
    //}
}

3. Reflection 사용

Reflection을 통해 객체를 복제할 수 있습니다. Activator.CreateInstance를 사용하면 클래스의 멤버변수를 조회하고 값을 복제할 수 있습니다.

public class Person : ICloneable
{
    ...

    public object Clone()
    {
        //인스턴스 생성
        Type type = this.GetType();
        Person clone = (Person)Activator.CreateInstance(type);

        //인스턴스의 필드를 조회하고, 값 복사
        foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
            field.SetValue(clone, field.GetValue(this));
        return clone;
    }
}

장점은 모든 맴버변수를 조회하고 복사하는 작업을 자동화할 수 있기 때문에 개발자가 실수할 여지가 적다는 것입니다. 유지보수 과정에서 멤벼변수를 추가하거나 삭제하더라도 코드를 수정할 필요가 없습니다. 또 다른 장점으로는 Deep Copy를 손쉽게 구현할 수 있습니다. 이에 관해서는 링크를 참고해주세요.

단점으로는 성능이 상당히 저하된다는 점입니다. 상황에 따라 다르겠지만 수천번의 복제만으로 2-3초간의 지연이 발생할 수 있습니다. 따라서 Reflection을 사용할 때는 복제의 호출 횟수를 통제하는 것이 좋습니다. 다른 단점으로는 C#코드가 Partial Trust 모드로 실행되는 경우 Reflection을 사용할 수 없습니다. Full Trust 모드로 실행되지 않는 환경에서는 이에 관한 예외 처리가 필요합니다.

4. Serialization을 통한 복제

객체를 문자열로 직렬화한 후, 즉시 역직렬화하는 방법으로 객체를 복제할 수 있습니다. 이 방법은 복제가 자동으로 진행되므로 단순하고 개발자가 실수할 여지가 적다는 장점이 있습니다. 이 방법의 단점은 Reflection보다 훨신 느리다는 점입니다. XML, Soap, Binary, JSON 등 다양한 직렬화 방법 중에서 어떤 방법을 선택하지느에 따라 private 필드가 복제되지 않거나 의도와 다르게 복제될 수 있으니 주의가 필요합니다. 샘플코드는 여기, 여기, 여기를 참고하시기 바랍니다.

public class Person : ICloneable
{
    //Newtonsoft.Json을 통한 복제방법
    public object Clone()
    {
        string serializedStr = Newtonsoft.Json.JsonConvert.SerializeObject(this);
        Person personClone = Newtonsoft.Json.JsonConvert.DeserializeObject<Person>(serializedStr);
        return personClone;
    }
}

5. IL(중간언어)를 사용하여 복제코드 작성

.NET의 중간언어(Intermediate Language)를 사용하여 객체를 복제할 수 있습니다. 이 방법에 대해 간단히 설명하자면 DynamicMethod를 생성하여 ILGenerator를 얻고 나서, ILGenerator를 사용하여 Clone을 수행하는 코드를 생성하는 방법입니다. 이 방법은 Reflection 보다 빠르다는 장점이 있지만, 구현 방법이 복잡하기에 이해하고 관리하는데 있어 어려움이 있을 수 있습니다. 예제코드는 여기에서 확인해주세요.

6. 이미 구현된 Package를 통한 복제

구현된 다양한 도구들을 활용하여 객체를 복제할 수 있습니다. 외부 도구를 사용하기 떄문에 간편하지만 개발자가 통제할 수 있는 영역이 적고 성능저하나 보안문제도 발생할 수 있으므로 주의가 필요합니다. Cloen에 관한 다양한 Package를 살펴보시려면 여기를 확인하세요.

References

 

댓글