상세 컨텐츠

본문 제목

[C# 이해하기] 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)

개발기록/Computer Science

by 도리(Dory) 2026. 1. 3. 10:45

본문

“복사”라고 하면 보통 “완전히 똑같은 걸 하나 더 만든다”를 떠올립니다.

하지만 C#에서 복사는 생각보다 단순하지 않습니다. 왜냐하면 객체 안에 또 다른 객체(참조형)가 들어있을 수 있기 때문입니다.

 

그래서 복사에는 크게 두 가지가 있습니다.

 

  • 얕은 복사(Shallow Copy): 겉은 새로 만들지만, 안쪽 참조는 그대로 공유
  • 깊은 복사(Deep Copy): 안쪽 참조까지 새로 만들어 완전히 독립

 

 


1) 먼저, “복사 문제”가 왜 생기나?

 

값 형식(int, float 등)은 복사하면 그냥 값이 복사되니 직관적입니다.

문제는 참조 형식(class, 배열, List 등)입니다.

 

참조 형식의 객체는 “겉(객체)” 안에 필드들이 있고, 그 필드 중 일부가 또 다른 객체를 가리킬 수 있습니다.

 

즉, 이런 구조가 가능합니다.

 

  • Player 객체
    • Level (int)
    • Inventory (int[] 배열) ← 또 다른 참조형

 

이때 “Player를 복사했다”는 말이 Inventory도 새로 복사했다는 뜻인지가 애매해집니다.

이 애매함을 정리한 개념이 얕은 복사/깊은 복사입니다.

 


2) 정의

  • 얕은 복사: “겉 껍데기만 새로 만들고, 내부의 참조형은 같이 쓴다”
  • 깊은 복사: “내부의 참조형까지 새로 만들어서, 완전히 따로 쓴다”

3) 얕은 복사 예시: 겉은 둘, 속은 공유

class Player
{
    public int Level;
    public int[] Inventory; // 참조형(배열)
}

var p1 = new Player { Level = 1, Inventory = new[] { 10, 20 } };

// 얕은 복사(의미): Player는 새로 만들었지만 Inventory는 공유
var p2 = new Player { Level = p1.Level, Inventory = p1.Inventory };

p2.Level = 99;         // p2의 값 필드만 변경
p2.Inventory[0] = 777; // 공유된 배열을 변경

Console.WriteLine(p1.Level);        // 1   (영향 없음)
Console.WriteLine(p1.Inventory[0]); // 777 (영향 있음)

 

왜 이런 일이 생길까?

Inventory는 배열이고(참조형), p1.Inventory를 그대로 넣으면 p2.Inventory같은 배열을 가리키게 됩니다.

 


4) 깊은 복사 예시: 내부까지 새로 만들어 독립

var p3 = new Player
{
    Level = p1.Level,
    Inventory = (int[])p1.Inventory.Clone() // 배열 자체를 새로 복사
};

p3.Inventory[0] = 123;

Console.WriteLine(p1.Inventory[0]); // 777 (p3와 무관)
Console.WriteLine(p3.Inventory[0]); // 123

 

여기서는 Inventory 배열도 새로 만들었기 때문에, p3p1과 독립적입니다.

 


5) 심화 : “Clone() 했는데도 공유가 남는 경우”

배열이 int[]처럼 값형을 담으면 Clone()이 꽤 안전합니다.

그런데 배열이 Item[]처럼 참조형 객체를 담는 배열이면 얘기가 달라집니다.

 

class Item { public string Name; }
class Bag  { public Item[] Items; }

var b1 = new Bag { Items = new[] { new Item { Name = "Potion" } } };
var b2 = new Bag { Items = (Item[])b1.Items.Clone() }; // 배열만 새로, Item은 공유

b2.Items[0].Name = "Elixir";

Console.WriteLine(b1.Items[0].Name); // Elixir (같이 바뀜)

 

여기서 Clone()은 “배열”은 새로 만들지만, 배열 안의 Item 객체까지 새로 만들지는 않습니다.

그래서 b1.Items[0]b2.Items[0]같은 Item 객체를 가리키는 공유 상태가 됩니다.

 

 

“진짜 깊은 복사”로 고치면 이렇게 됩니다

 

배열만 복사하지 말고, 각 Item도 새로 만들어 담아야 합니다.

 

var b3 = new Bag
{
    Items = b1.Items.Select(item => new Item { Name = item.Name }).ToArray()
};

b3.Items[0].Name = "Elixir";

Console.WriteLine(b1.Items[0].Name); // Potion  ← 안 바뀜
Console.WriteLine(b3.Items[0].Name); // Elixir

 

핵심은 이 한 줄입니다.

 

  • 얕은 복사: Items = (Item[])b1.Items.Clone()  (Item 공유)
  • 깊은 복사: Items = b1.Items.Select(item => new Item{...}).ToArray() (Item도 복제)

 

 

“배열 요소가 값형이면?”

만약 Itemsint[] 같은 값형 배열이라면, Clone()만으로도 사실상 깊은 복사처럼 동작합니다.

왜냐하면 요소 자체가 값이라 “참조 공유”라는 문제가 없기 때문입니다.

 

  • int[]Clone()이면 요소 값이 그대로 복사(안전)
  • Item[]Clone()이면 요소 “참조”가 그대로 복사(공유 위험)

 


 

6) 언제 얕은 복사가 문제이고, 언제 괜찮나?

 

얕은 복사가 “나쁘다”가 아니라, 공유가 의도와 맞는지가 중요합니다.

 

 

얕은 복사가 위험해지는 경우

  • 복사본을 수정했는데 원본도 같이 바뀌면 버그가 되는 상황

 

얕은 복사가 오히려 좋은 경우

  • 큰 데이터를 매번 복사하면 비용이 커서, 읽기 전용으로 공유하는 게 합리적인 상황
  • 내부 객체가 불변(immutable)이거나 수정되지 않는다고 보장되는 상황

관련글 더보기