C#
C# 아이콘

깊은 복사와 얕은 복사의 차이점을 모르고 프로젝트를 진행할 경우 치명적인 오류를 발생 시킬 수 있습니다. 어떠한 객체의 상태를 변경했을 때 의도한 것은 하나의 객체의 상태만 변경하는 것이 였는데 다른 객체의 상태도 바뀌어 버리는 경우를 예를 들 수 있습니다. 이유도 모른 채 이러한 버그가 생기면 해결하는데 많은 시간을 소비하기에 중요한 이론이기도 합니다.


[깊은 복사란?]

원본을 복사 시 완전히 독립된 복사본을 생성하는 과정 입니다. 객체1을 복사해서 객체2가 복사 되었다고 했을 때 객체1의 상태가 변경되거나 삭제 되어도 객체2에는 영향을 미치지 않는 것을 말합니다.

하는 방법은 C#의 경우 3가지가 존재합니다.

  1. 수동 복사
    • 직접 객체를 복사하는 방법입니다. 하위 객체도 모두 복사해야 하기 때문에 실수 하기 쉽습니다. 하지만 정교하게 커스타마이징이 가능하다는 장점이 있습니다.
  2. 복사 생성자 또는 복사 메서드 사용
    • C#에서는 깊은 복사를 해줄 수 있는 라이브러리가 제공 되기에 해당 라이브러리를 사용하면 간편하게 복사가 가능 합니다.
  3. 직렬화와 역직렬화
    • 객체를 직렬화 한 뒤 이것을 다시 역직렬화 하면 새로운 인스턴스를 만들어 내게 됩니다.

위 방법들의 예시를 하나씩 코드로 작성해 보겠습니다.

수동 복사 예시

using System;
using System.Collections.Generic;

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

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public Person DeepCopy()
    {
        return new Person(Name, Age);
    }
}

class Program
{
    static void Main()
    {
        Person person1 = new Person("Alice", 30);
        List<Person> originalList = new List<Person> { person1 };

        List<Person> copiedList = new List<Person>();
        foreach (var person in originalList)
        {
            copiedList.Add(person.DeepCopy());
        }

        Console.WriteLine(originalList[0].Name);
        Console.WriteLine(copiedList[0].Name);

        copiedList[0].Name = "Bob";

        Console.WriteLine(originalList[0].Name);
        Console.WriteLine(copiedList[0].Name);
    }
}

생성자 내부에서 모든 변수를 대입하여 객체를 생성 시키는 방법입니다. DeepCopy 내부에 new Person(Name, Age) 생성자를 사용한 부분이 핵심 입니다.

ICloneable 인터페이스 사용

using System;

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

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    public object Clone()
    {
        return new Person(Name, Age);
    }
}

class Program
{
    static void Main()
    {
        Person person1 = new Person("Alice", 30);
        Person copiedPerson = (Person)person1.Clone();

        Console.WriteLine(person1.Name);
        Console.WriteLine(copiedPerson.Name);

        copiedPerson.Name = "Bob";

        Console.WriteLine(person1.Name);
        Console.WriteLine(copiedPerson.Name);
    }
}

ICloneable을 사용하여 Clone메서드를 부르면 손쉽게 복사가 가능합니다. 간결하고 사용하기도 쉬워서 가장 추천드리는 방법입니다.

Newtonsoft.Json 라이브러리를 활용한 직렬화와 역직렬화

using Newtonsoft.Json;
using System;

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

class Program
{
    static void Main()
    {
        Person person1 = new Person { Name = "Alice", Age = 30 };

        // 객체를 JSON 문자열로 직렬화
        string json = JsonConvert.SerializeObject(person1);

        // JSON 문자열을 객체로 역직렬화
        Person copiedPerson = JsonConvert.DeserializeObject<Person>(json);

        Console.WriteLine(person1.Name);
        Console.WriteLine(copiedPerson.Name);

        copiedPerson.Name = "Bob";

        Console.WriteLine(person1.Name);
        Console.WriteLine(copiedPerson.Name);
    }
}

직렬화와 역직렬화 자체가 성능에 영향을 주는 작업이기에 추천 드리지는 않습니다. 하지만 직렬화를 한 후 역직렬화를 하면 왜 깊은 복사가 되는지 않는 것은 중요합니다. 직렬화는 객체를 텍스트로 변환해 주는 작업이며 역직렬화는 텍스트를 객체로 변환해 주는 작업입니다. 즉, 텍스트로 변환 후 다시 역직렬화 된 객체는 아예 다른 것이라는 뜻 입니다.

[얕은 복사란?]

복사 본이 원본 객체를 참조하게 되는 복사 방법을 말합니다. 이를 하는 방법은 간단합니다. 대입을 통한 복사를 하면 됩니다.

public class Person
{
    public string Name { get; set; }

    public Person(string name)
    {
        Name = name;
    }
}

public class ShallowCopyExample
{
    public static void Main()
    {
        // 원본 객체 생성
        Person originalPerson = new Person("Alice");

        // 얕은 복사 수행 (대입 연산자 사용)
        Person shallowCopy = originalPerson;

        // 복사된 객체의 이름 변경
        shallowCopy.Name = "Bob";

        // 원본 객체와 복사된 객체의 이름 출력
        Console.WriteLine("Original Name: " + originalPerson.Name);  // 출력: "Original Name: Bob"
        Console.WriteLine("Shallow Copy Name: " + shallowCopy.Name);  // 출력: "Shallow Copy Name: Bob"
    }
}

위 코드를 실행 해 보면 복사된 객체를 변경 했음에도 원본 객체도 변경된 것을 확인 할 수 있습니다.

값타입과 참조 타입의 얕은 복사

얕은 복사를 하게 되면 복사된 객체의 변수를 변경하여도 원본의 변수가 변경되지 않는 경우가 있습니다. 이를 값타입의 변수라고 합니다.

값타입의 변수는 구조체와, 정수형 변수, float형 변수 double형 변수 등이 있습니다. 반대로 원본의 변수가 변경되는 경우는 참조 타입이라고 하며 클래스와 string이 대표적 입니다.

public struct Point
{
    public int X;
    public int Y;
}

public class ShallowCopyExample
{
    public static void Main()
    {
        Point originalPoint = new Point { X = 10, Y = 20 };
        Point shallowCopy = originalPoint;

        shallowCopy.X = 30;

        Console.WriteLine("Original Point: X = " + originalPoint.X + ", Y = " + originalPoint.Y);
        Console.WriteLine("Shallow Copy: X = " + shallowCopy.X + ", Y = " + shallowCopy.Y);
    }
}

위와 같이 구조체는 값타입 이기 때문에 복사본을 변경해도 원본이 변경되지 않습니다.

public class Person
{
    public string Name { get; set; }
}

public class ShallowCopyExample
{
    public static void Main()
    {
        // 원본 객체 생성
        Person originalPerson = new Person { Name = "Alice" };

        // 얕은 복사
        Person shallowCopy = originalPerson;

        // 복사된 객체의 이름 변경
        shallowCopy.Name = "Bob";

        // 원본 객체와 복사된 객체의 이름 출력
        Console.WriteLine("Original Name: " + originalPerson.Name);  // 출력: "Original Name: Bob"
        Console.WriteLine("Shallow Copy Name: " + shallowCopy.Name);  // 출력: "Shallow Copy Name: Bob"
    }
}

문자열은 참조 타입이기에 원본도 같이 변경 됩니다.

[얕은 복사와 깊은 복사의 차이점]

얕은 복사는 참조를 복사하기 때문에 참조형의 변수들에게 영향이 갑니다. 좀 더 자세히 말하자면 원본의 메모리 공간을 복사된 객체가 가르키고 있는 형태가 되는 것 입니다. 하지만 깊은 복사는 복사된 객체가 별도의 메모리 공간에 저장이 됩니다. 가장 큰 차이점은 별도의 메모리 공간을 가지는지 아니면 원본의 메모리를 참조하는 형태인지 입니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

public class Person
{
    public string Name { get; set; }
}

public class ShallowCopyExample
{
    public static void Main()
    {
        // 원본 객체 생성
        Person originalPerson = new Person { Name = "Alice" };

        // 얕은 복사
        Person shallowCopy = originalPerson;

        // 복사된 객체의 이름 변경
        shallowCopy.Name = "Bob";

        // 원본 객체와 복사된 객체의 이름 출력
        Console.WriteLine("Original Name: " + originalPerson.Name);  // 출력: "Original Name: Bob"
        Console.WriteLine("Shallow Copy Name: " + shallowCopy.Name);  // 출력: "Shallow Copy Name: Bob"
    }
}

public class DeepCopyExample
{
    public static void Main()
    {
        // 원본 객체 생성
        Person originalPerson = new Person { Name = "Alice" };

        // 깊은 복사
        Person deepCopy = new Person { Name = originalPerson.Name };

        // 복사된 객체의 이름 변경
        deepCopy.Name = "Bob";

        // 원본 객체와 복사된 객체의 이름 출력
        Console.WriteLine("Original Name: " + originalPerson.Name);  // 출력: "Original Name: Alice"
        Console.WriteLine("Deep Copy Name: " + deepCopy.Name);        // 출력: "Deep Copy Name: Bob"
    }
}

위와 같이 얕은 복사는 메모리자체를 참조하기 때문에 참조 타입(메모리 공간을 가지는) 문자열 변수가 변경되면 원본도 같이 변경되는 것 입니다. 반대로 깊은 복사는 메모리 공간 자체를 별도로 가지기 때문에 원본과 복사본은 아예 다른 객체라고 할 수 있습니다.

[마무리]

헷갈린다고 대충이해 하고 넘어가면 나중에 큰 실수를 하는 경우가 생깁니다. 그러므로 예시코드를 통해서 정확한 이해가 필요합니다. 어떤 값들이 복사본에 영향을 미치는지 모르고 함부로 객체를 변경 했다가 큰 상황으로 번질일이 있기 때문입니다. 이번 글을 통해서 정확한 이해를 하시길 바랍니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다