멈추지 않고 끈질기게

[기타] 얕은 복사 vs 깊은 복사 본문

기타

[기타] 얕은 복사 vs 깊은 복사

sam0308 2023. 1. 28. 11:58

※ 해당 포스팅은 개인의 공부 정리용 글입니다. 틀린 내용이 있다면 추후 수정될 수 있습니다.

 

프로그래밍에서 객체를 복사할 때는 얕은 복사와 깊은 복사의 두가지 방식이 있습니다.

이번 포스팅에서는 해당 내용에 관하여 알아보겠습니다.

 

1. 얕은 복사(Shallow Copy)

얕은 복사는 해당 객체의 주소 값만을 복사하고, 새로운 인스턴스를 생성하지는 않습니다. 복사 후 대상 객체와 복사한 객체가 동일한 인스턴스를 가리키기 때문에, 서로 종속적인 관계가 됩니다.

class Item
{
    public int id;
    public string name;
    public float value;
    public int price;

    public Item(int id, string name, float value, int price)
    {
        this.id = id;
        this.name = name;
        this.value = value;
        this.price = price;
    }
}

static void Main(string[] args)
{
    Item item1 = new Item(1001, "체력 포션", 50.0f, 100);
    Item item2 = item1;
    item1.name = "마나 포션";
    item2.price = 500;

    Console.WriteLine($"item1 - ID: {item1.id}, name: {item1.name}, value: {item1.value}, price: {item1.price}");
    Console.WriteLine($"item2 - ID: {item2.id}, name: {item2.name}, value: {item2.value}, price: {item2.price}");
}

Main 함수 안을 보면 Item 클래스의 Item1 인스턴스를 생성한 뒤, item2에 단순히 item1 변수를 넣어 얕은 복사를 실행하였습니다. 이 후 item1과 item2의 변수를 각각 수정하였습니다.

 

그림1. 얕은 복사

출력 결과를 보면 item1과 item2의 변경사항이 서로에게 똑같이 적용되어 있음을 알 수 있습니다. 얕은 복사는 대상 객체의 주소값을 복사할 뿐 새롭게 메모리를 할당받는 것이 아니기 때문입니다. 이를 그림으로 표현하면 다음과 같습니다.

 

그림2. 얕은 복사(도식)

Item1은 new 키워드를 통해 선언하면서 힙(Heap) 영역에 새로운 인스턴스를 생성하고 그 주소를 가리킵니다. item2는 여기서 단순히 item1의 주소 값만을 복사했을 뿐, 새로운 인스턴스를 생성하지는 않습니다. item1과 item2가 완전히 동일한 인스턴스를 가리키고 있기 때문에, item1의 변수를 수정하든 item2의 변수를 수정하든 동일하게 적용되는 것입니다.

 

2. 깊은 복사(Deep Copy)

깊은 복사는 해당 객체와 동일한 변수들을 가지는 인스턴스를 새로 생성하여 복사하는 방식입니다. 복사한 객체가 대상 객체와 별개의 인스턴스를 가리키기 때문에, 얕은 복사와 달리 서로 비종속적인 관계가 됩니다.

class Item
{
    public int id;
    public string name;
    public float value;
    public int price;

    public Item(int id, string name, float value, int price)
    {
        this.id = id;
        this.name = name;
        this.value = value;
        this.price = price;
    }

    public Item Copy()
    {
        return new Item(id, name, value, price);
    }
}

static void Main(string[] args)
{
    Item item1 = new Item(1001, "체력 포션", 50.0f, 100);
    Item item2 = item1.Copy();
    item1.name = "마나 포션";
    item2.value = 5.0f;

    Console.WriteLine($"item1 - ID: {item1.id}, name: {item1.name}, value: {item1.value}, price: {item1.price}");
    Console.WriteLine($"item2 - ID: {item2.id}, name: {item2.name}, value: {item2.value}, price: {item2.price}");
}

깊은 복사를 위해 Item 클래스에 Copy() 함수를 추가하였습니다. 해당 함수는 자기 자신의 변수들을 생성자로 넘겨 새로운 Item 인스턴스를 생성한 뒤 반환합니다. 이번에는 item2에 item1.Copy()를 통해 깊은 복사를 실행하여 할당하였고, 마찬가지로 item1과 item2의 변수를 각각 수정하였습니다.

 

그림3. 깊은 복사

아까와는 다르게 item1과 item2의 수정 내용이 각각 자신에게만 반영된 모습을 볼 수 있습니다. item2가 아예 새롭게 생성한 인스턴스를 가리키고 있기 때문입니다. 이를 그림으로 표현하면 다음과 같습니다. 

 

그림4. 깊은 복사(도식)

Item1은 new 키워드를 통해 선언하면서 힙(Heap) 영역에 새로운 인스턴스를 생성하여 그 주소를 가리키고, item2는 item1의 Copy() 함수를 호출하여 item1을 복사하고 그 주소를 가리킵니다. Copy() 함수에서 new 키워드로 새로운 인스턴스를 생성했기 때문에, item2는 이제 item1과 다른 별개의 인스턴스를 가리킵니다. 따라서 item1에서 변수를 수정하면 item1에만 적용되고, item2에서 변수를 수정하면 item2에만 적용되는 것입니다.

 

3. 번외

이번 내용을 정리하기로 마음먹은 계기가 있습니다. 하기 코드는 제가 작성했던 포폴용 프로젝트의 코드 일부입니다.

//Prefers 에서 플레이어 데이터 가져옴
public void SetStatus(PlayerStatus stat)
{
    baseStat = stat;
    curStat = stat;
}

Prefers 클래스에서 PlayerStatus 객체를 해당 클래스에 할당해주기 위한 함수입니다. 여기서 캐릭터 기본 스탯을 저장해두기 위한 baseStat과 플레이 중 악세사리 습득 시 증가하는 스탯을 갱신하기 위한 curStat을 별도로 관리하려고 했는데, 보다시피 stat 객체를 양쪽 모두에 얕은 복사로 전달하였습니다. 추후 악세사리 습득 후 스탯이 의도한 양과 다르게 증가하는 이슈가 발견되어 디버깅하던 중에 발견하게 되었고, PlayerStatus 클래스에 깊은 복사를 위한 함수를 추가하여 해결하였습니다. 나름 얕은 복사와 깊은 복사의 차이를 알고 있다고 생각하였는데 이런 실수를 한 것이 기억에 남아, 이번 포스팅에서 해당 내용을 다시 정리하게 되었습니다.