c# 깊은 복사, 얕은 복사, 할당의 차이
객체는 밸류 타입과 레퍼런스 타입으로 나뉜다. 깊은 복사, 얕은 복사, 할당은 레퍼런스 타입에서만 문제되며 특히 얕은 복사와 할당이 문제된다.
깊은 복사
레퍼런스 타입 객체를 deep copy하면 refer하는 객체와 refer된 밸류가 모두 새롭게 만들어진다. 레퍼런스만 복사하는 건 쉽지만 밸류까지 복사하는 건 어렵다. 시리얼라이즈를 한 뒤 디시리얼라이즈를 하던가 iCloneable 인터페이스를 써야 한다. 이런 문제는 주로 클래스를 복사할 때 겪게 되는데 이렇게 복잡하게 하느니 그냥 struct를 쓰는 게 낫다. 클래스를 쓸 것인가 스트럭트를 쓸 것인가 판단할 때 중요한 기준들 가운데 하나가 밸류를 복사할 일이 있는가 하는 거다.
얕은 복사
깊은 복사가 개념적으로는 비교적 단순하지만 구현이 어려운 것과 반대로 shallow copy와 assign은 개념적으로 약간 더 복잡한 대신 구현은 간단하다. 책을 봐도 그렇고 검색을 해 봐도 그렇고 얕은 복사에 대한 설명들이 부족하다. 그저 객체에 대한 레퍼런스를 복사하여 복사를 한 뒤에도 원래의 객체가 refer하는 밸류를 같이 refer한다는 수준에 대부분 그치고 있다. 틀린 설명은 아닌데 뒤에 더 중요한 내용이 빠져 있다.
할당
우선 그 전에 용어를 좀 정리할 필요가 있는데 c#의 경우 ‘=’를 assignment operator 즉 할당 연산자라 한다. assign은 ‘할당한다’, ‘배정한다’는 뜻인데 개념적으로는 ‘대입한다’는 표현이 더 쉽게 이해된다. 그러나 대입이라는 뜻의 단어인 substitute라고 하지는 않는다. 아래 예제는 레퍼런스 타입인 클래스를 다른 클래스에 할당하는 예제다. 마치 복사되는 거처럼 보인다. 할당된 클래스의 값을 바꾸면 할당한 원래의 클래스까지 값이 바뀐다. 이들은 복사된 게 아니라 ‘연결’된 거기 때문이다.
class Class1
{
public int Int1;
}
private void button1_Click(object sender, EventArgs e)
{
Class1 class1 = new();
class1.Int1 = 1;
Method1(class1);
Text = class1.Int1.ToString(); // 2
}
void Method1(Class1 class1)
{
Class1 newClass1 = new();
newClass1 = class1;
newClass1.Int1 = 2;
}
분명히 새로운 클래스 인스턴스를 만든 뒤 그것에 할당을 했음에도 원래의 인스턴스와 새로운 인스턴스는 서로 연결되기 때문에 하나를 바꾸면 다른 것도 바뀌는 것이다. 이 부분에 대해 c# 레퍼런스는 좀 애매한 설명을 하고 있는데 할당을 통해 밸류에 대한 레퍼런스가 복사된다고 나와 있다. 두 개의 클래스 인스턴스들이 모두 같은 밸류를 refer하니까 같은 레퍼런스를 갖고 있는 건 맞는데 이렇게 이해하면 얕은 복사와 결국 같게 된다. 이와 달리 파이썬 레퍼런스는 좀 더 정교하게 설명하고 있는데 binding이라 기술하고 있다. 이게 맞는 말이다. 레퍼런스만 복사하느냐와 연결까지 하느냐의 차이는 복사된 것의 값을 바꾼 때 원래의 것도 바뀌느냐 바뀌지 않느냐 하는 중요한 결과의 차이에 이르게 된다.
아래의 예제는 Object.MemberwiseClone를 이용하여 얕은 복사를 한 것이다. 복사된 클래스의 값을 바꾸어도 원래 클래스의 값은 바뀌지 않는다. 실무에서는 이렇게 클래스를 복사를 하기는 하지만 원래의 클래스와는 단절시켜야 할 때가 많다. 이럴 때 ‘=’를 써서 대입을 시켜 버리면 의도한 대로 구현할 수 없다.
class Class1
{
public int Int1;
public Class1 Clone()
{
return (Class1)this.MemberwiseClone();
}
}
private void button2_Click(object sender, EventArgs e)
{
Class1 class1 = new();
class1.Int1 = 1;
Method2(class1);
Text = class1.Int1.ToString(); // 1
}
void Method2(Class1 class1)
{
Class1 newClass1 = class1.Clone();
newClass1.Int1 = 2;
}
문제는 이러한 얕은 복사에 대한 c# 레퍼런스의 설명이다. ‘레퍼런스가 복사된다’고 할당 연산자를 설명할 때와 같게 나와 있다. 맞는 말이긴 하다. 할당과 얕은 복사 모두 레퍼런스를 복사하기는 하기 때문이다. 여기까지만 보면 이들은 같다. 정리하면 이렇다. 할당은 새로운 객체에 레퍼런스를 복사한 뒤 이들을 연결한다. ‘=’의 의미 그대로 사실상 같은 걸로 만들어 버린다. 얕은 복사는 새로운 객체에 레퍼런스를 복사한 뒤 그냥 그거로 끝난다. 새로운 객체에 레퍼런스가 복사된 순간까지는 원래의 객체와 구별이 되지 않는다. 둘 모두 같은 밸류를 가리키고 있기 때문이다. 그러나 위 예제에서처럼 새로운 객체의 값을 바꾸는 순간 이건 자신만의 갈 길을 가게 되는 거다.
얕은 복사가 된 것은 복사된 순간까지는 원래의 객체와 같지만 그 값을 바꾸게 되면 독립적인 객체가 되어 원래의 객체 값에 영향을 주지 않고 새로운 밸류를 refer하게 된다.