2017. 10. 18.

[C#] 메모리 관리와 가비지 콜렉션(garbage collection) - 가비지 콜렉션을 유발하는 예시

개요

이 글은 다음과 같은 항목으로 구성되어 있습니다. 그리고 오늘은 가비지 콜렌셕을 정리하도록 겠습니다.




지금까지 Type에 대한 이해를 바탕으로 메모리 할당과 해제 대해서 알아보았습니다. 가비지 콜렉션이 발생하는 경우에 대해서도 이미 설명하였습니다. 이번에는 클래스의 instance가 참조하는 구조체와 같은 실제 상황을 예시로 지금까지의 내용을 적용해서 설명하려고 합니다. 그리고 이번 글을 끝으로 C#의 기본적인 가비지 콜렉션에 관한 연재는 마치도록 하겠습니다.

클래스에서 참조하는 구조체

Windows Forms 애플리케이션은 예시로 설명하도록 하겠습니다. Windows Forms를 예시로 사용하지만 Windows Forms이 무엇인지는 중요하지 않습니다. 클래스와 구조체가 메모리에서 어떻게 다르고 관리되는지를 알고 있기만 하면 전혀 새로운 내용은 없습니다. Type에 대해서 잘 이해하고 있고, 메모리가 어떻게 할당되고 해제되는지를 알고 있다면 오늘 설명은 복습하는 기분으로 보고 넘어갈 수 있습니다.


먼저 간단한 코드로 클래스와 코드를 보겠습니다.


Size s = new Size (100, 100);          // 구조체. value type입니다.
Font f = new Font ("Arial",10);        // 클래스. reference type입니다.


여기서 Font 클래스는 중에는 Size 구조체도 있습니다. 그림으로 보면 좀더 구체적으로 알 수 있습니다.


먼저 Font의 instance가 하나 생성되었습니다. Font는 클래스, 즉 reference type이므로 힙(heap)에 생성됩니다. 그리고 reference type을 가리키는 변수 f는 스택(stack)에 생성됩니다. 이전 글에서 설명하였듯이 reference type을 가리키는 변수는 스택에 생성됩니다. Size가 하나 생성되었습니다. Size는 구조체, 즉 value type이므로 스택에 생성됩니다. 이 구조체를 가리키는 변수 s도 역시 스택에 생성됩니다.


reference type인 클래스의 instance만 힙에 생성되었고, 나머지는 모두 스택에 생성된 상황입니다.


여기서 새로운 클래스 Form이 등장합니다. Form은 Size와 Font를 멤버변수로 가지는 클래스입니다. Form의 instance를 하나 생성하겠습니다.


Form myForm = new Form();


Form의 멤버변수 Size와 Font에 앞의 변수 s와 f를 대입하도록 하겠습니다.


myForm.Size = s;
myForm.Font = f;


지금까지의 메모리 상황을 그림으로 살펴보면 다음과 같습니다.




그림을 보면 새롭게 힙에 생성된 Form instance를 볼 수 있습니다. Form은 Size와 Font를 멤버변수로 가지고 있습니다. reference type을 가리키는 변수 myForm은 스택에 생성되어 있습니다. 그리고 myForm의 멤버변수 Size는 구조체이지만 reference type인 Form 클래스에 포함된 것이므로 힙에 생성됩니다. Font도 Form 클래스에 포함된 것이므로 힙에 생성됩니다.


그리고 myForm.Size에 미리 생성해둔 s를 대입합니다. 이전에 설명드렸듯이 value type을 대입하면 값이 복사됩니다. 즉, Size s의 복사본이 힙에 생성됩니다. myForm.Font에도 미리 생성해둔 f를 대입합니다. reference type의 변수를 대입하면 복사본이 생성되지 않고 reference가 그대로 대입됩니다. 지금까지 설명한 내용이 모두 적용된 결과가 바로 위의 그림입니다.


그러므로 myForm.Size는 s가 가리키는 Size와는 별개의 구조체입니다. myForm.Font는 f가 가리키는 Font와 동일합니다. 만약 s를 통해서 Width, Height를 수정하면 myForm.Size에 전혀 영향을 주지 못합니다. f를 통해서 FontFamily, Size 등을 수정하면 myForm.Font도 수정됩니다.

가비지 콜렉션을 유발하는 예시

코드를 먼저 살펴보겠습니다.


public class ExampleScript {
   float[] RandomList(int numElements) {
       var result = new float[numElements];
       
       for (int i = 0; i < numElements; i++) {
           result[i] = Random.value;
       }
       
       return result;
   }
}


기능은 매우 단순합니다. int로 갯수를 전달하면, 그 수 크기의 배열을 힙에 생성합니다. 만약 이 함수가 매우 잦게 호출된다면 어떻게 될까요? 호출될 때마다 배열이 생성될 것이고, 이 배열의 참조를 유지하지 않는다면 이 배열은 가비지가 됩니다. 이렇게 생성되는 가비지를 막으려면 다음과 같이 수정하는 것이 좋습니다.


public class ExampleScript {
   void RandomList(float[] arrayToFill) {
       for (int i = 0; i < arrayToFill.Length; i++) {
           arrayToFill[i] = Random.value;
       }
   }
}


차이는 간단합니다. 앞의 코드에서는 함수 내에서 새로운 배열을 만든 반면에, 이 코드에서는 배열을 매개변수로 전달받습니다. 더이상 힙에 새로운 배열이 생성되지 않습니다. 인자로 전달한 변수에 대한 참조만 잘 유지된다면, 이 배열은 가비지가 되지 않을 것입니다.


또 다른 예를 살펴보겠습니다. 다음 함수는 매 프레임 실행되는 함수입니다.


void Update()
{
   List myList = new List();
   DoSomething(myList);
}


단순한 함수입니다. 매 프레임마다 새로운 List를 생성하고, 이 List를 가지고 어떤 일을 합니다. List는 당연히 클래스입니다. 그러므로 매 프레임 우리는 List를 힙에 생성하고, Update() 함수를 빠져나오면 생성된 List는 모두 유효한 참조가 없으므로 가비지가 됩니다. 가비지를 생성하지 않도록 코드를 수정하면 다음과 같습니다.


private List myList = new List();
void Update()
{
   myList.Clear();
   DoSomething(myList);
}


Update() 함수가 매 프레임 실행되므로 이를 계속 생성하지 않고, 차라리 멤버변수로 가지고 있습니다. 이제 힙에는 한 개의 List만 생성됩니다. 매 프레임 이 List를 비워서 재사용합니다.


이 외에도 가바지를 유발하는 예시는 구글링하여 쉽게 찾을 수 있습니다. 각 예시를 보면서 지금까지 다룬 내용을 바탕으로 따져보면 좋을 것입니다.

정리하며


지금까지 우리는 C#의 Type, 메모리 할당과 해제를 통해서 가비지가 어떻게 생성되는지 알아보았습니다. 매 프레임 적은 양으로 생성되는 가비지는 언제 수거되어 메모리를 해제할지 우리는 알 수 없습니다. 문제는 가비지를 수집하고 메모리를 해제하는 것이 결코 가벼운 일이 아니라는 것입니다. 특히 CPU가 바쁘게 돌아가는 중에 가비지 콜렉션이 동작하면 이용자에게 불편함을 줄 수도 있습니다. 평소 가비지를 생성하지 않는 코드를 작성하는 습관을 가질 수 있도록 노력해야 겠습니다.

댓글 없음:

댓글 쓰기