업무를 진행하다가 예상하지 못한 이슈를 발견했었고 이를 해결하면서
다시 실수하지 않기 위해 정리 했습니다.
1. 무슨 문제가 발생했었는데??
문제 상황을 예시로 설명하면,
상점에 아이템이 100개가 있고 100개 중에 10개를 뽑은 후 특수 기술이 적용되어 있는지 체크를 해주어야 하는 기능을 구현 했는데
특수 기술이 적용되지 않은 아이템에도 특수 기술이 적용되어 나오는 문제가 발생했다.
즉,
뽑은 10개의 아이템 중 A라는 아이템을 2번 뽑았고
처음 뽑은 A에는 특수 기술이 존재하고 두 번째로 뽑은 A에는 특수 기술이 존재하지 않는데
두 번째로 뽑은 A에 특수 기술이 존재하는 이슈가 발생한 것이다.
위의 문제가 위험하고 중요하다고 생각하는 게 데이터가 꼬이는 것 뿐 에러가 발생하는 것이 아니기 때문에 자세히 확인하기 않으면 놓칠 수 있기 때문이다.
2. 얕은 복사와 깊은 복사
위 문제의 해결 방법은 간단했다.
참조 형식인 아이템 class를 복사할 때 얕은 복사가 아닌 깊은 복사로 처리하는 것이다.
대체 깊은 복사와 얕은 복사가 무엇이길래 이런 문제가 발생한 걸까?
무엇인지 아래에서 확인해보자!
얕은 복사
- 객체의 참조 값(주소)을 복사
- 참조 값을 복사했기 때문에 같은 객체를 바라봄
아래 예제 코드를 보면
Student class가 있고 b 인스턴스 변수가 a 인스턴스 변수를 복사한다.
이 때 얕은 복사가 이루어지는데 위에 설명했던 대로 객체의 참조 값만 복사 되기 때문에
a 변수가 가지고 있던 Student 객체의 주소 값을 b가 가지게 된다.
이로 인해
a 와 b 둘 다 Heap영역에 있는 동일한 객체를 바라보게 되고 b 값을 수정하면 a의 값도 변하게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Student
{
public string _name;
public int _age;
}
class Program
{
static void Main(string[] args)
{
Student a = new Student
{
_name = "Night",
_age = 27
};
// a를 b에 얕은 복사
Student b = a;
// b 값을 수정
b._name = "Owl";
b._age = 24;
Console.WriteLine($"A 학생 이름: {a._name}, 나이: {a._age}");
Console.WriteLine($"B 학생 이름: {b._name}, 나이: {b._age}");
}
}
1
2
3
출력 결과
A 학생 이름: Owl, 나이: 24
B 학생 이름: Owl, 나이: 24
왜 이런 일이 발생할까??
C#은 값 형식, 참조 형식 두 형식으로 나누어져 있다고 볼 수 있다.
참조 형식인 class는
1
2
3
4
5
Student a = new Student
{
_name = "Night",
_age = 27
};
위와 같이 객체를 생성할 때 Heap에 생성한 객체의 주소 값을 가지게 되는데(이를 “객체를 참조했다.” 라고 한다.)
이 때 Student b = a; 를 통해 a의 값을 복사하면 b는 a가 가지고 있던 객체의 주소 값을 가져오게 된다.
그러므로, b와 a는 동일한 객체를 바라보게 되는 거고 b의 값을 바꾸면 자연스럽게 a의 값도 바뀌게 되는 것이다.
1
2
class 에서 값 형식인 struct 로 변경해서 비교해본다면
더 쉽게 이해할 수 있다.
깊은 복사
- 객체의 실제 값을 복사
- Heap 영역에 새로운 메모리 공간을 생성해 새로운 객체를 생성
코드를 보면 Clone() 또는 DeepCopy() 메소드를 통해 새로운 객체를 생성해서 넘겨주고 있다.
두 메소드는 동일하게 객체의 실제 값 즉, 변수들의 값을 복사해 새로운 객체를 반환하고 있다.
이렇게 반환한 객체는 Heap 영역에 새로운 메모리 공간을 차지하게 되고 a 와 b는 서로 다른 객체를 바라보게 된다.
그 결과,
1
2
b._name = "Owl";
b._age = 24;
위의 코드로 b의 값을 변경하더라도 a의 값에는 영향을 주지 않게 된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Student : ICloneable
{
public string _name;
public int _age;
/// <summary>
/// MSDN 인터페이스에 맞추어 깊은 복사
/// </summary>
/// <returns></returns>
public object Clone() => new Student
{
_name = this._name,
_age = this._age
};
/// <summary>
/// 깊은 복사
/// </summary>
/// <returns></returns>
public Student DeepCopy() => new Student
{
_name = this._name,
_age = this._age
};
}
class Program
{
static void Main(string[] args)
{
Student a = new Student
{
_name = "Night",
_age = 27
};
Student b = a.Clone() as Student; // MSDN 인터페이스에 맞춘 깊은 복사
// Student b = a.DeepCopy(); // 깊은 복사
b._name = "Owl";
b._age = 24;
Console.WriteLine($"A 학생 이름: {a._name}, 나이: {a._age}");
Console.WriteLine($"B 학생 이름: {b._name}, 나이: {b._age}");
}
}
1
2
3
출력 결과
A 학생 이름: Night, 나이: 27
B 학생 이름: Owl, 나이: 24
결론
위 예제 코드를 보면 둘 다 코드적으로는 문제가 없는 코드이다.
하지만 결과에는 큰 차이가 있다.
얕은 복사와 깊은 복사는 학교에서도 배우는 만큼 중요하고 기본적인 내용이다.
실무에서 이 내용을 이슈로 경험을 해보니 역시 기본기가 가장 중요하다 라는 것을 다시 한번 깨닫게 되었다.