2017. 10. 15.

[C#] 메모리 관리와 가비지 콜렉션(garbage collection) - Type에 대한 이해

들어가기

저의 첫 프로그래밍 언어는 C였습니다. C를 공부하면서 C++로 공부하기 시작하였습니다. 이후 C#을 사용하게 되었고, C 계열 언어라서 쉽게 사용할 수 있을 거라고 생각했습니다. 하지만 겉보기와는 다르게 C#은 C나 C++과 완전히 다른 특성의 언어라는 것을 얼마되지 않아서 알게 되었습니다. 특히 Value 타입과 Reference 타입에 대한 접근 방식은 생소하였습니다. 이 개념은 가비지 콜렉션(Garbage Collection)과도 관련된 개념이라서 중요합니다. 이번 글은 C#의 Value 타입과 Reference 타입의 차이를 살펴볼 것입니다. 이 차이를 이해해야 C#의 메모리 관리 및 가비지 콜렉션에 대해서 알 수 있습니다. 참고로 이 글은 C#에 대한 기초 지식이 필요합니다.

개요

이 글은 다음과 같은 항목으로 구성될 예정입니다.




이번 글에서는 Type에 대해서 알아보겠습니다.

구조체(struct)

먼저 구조체(struct)를 살펴보겠습니다. 구조체가 무엇일까요? 상속과 객체를 소멸시킬 수 있는 finalizer를 지원하지 않는 클래스(class)를 상상해봅시다. 그게 바로 구조체입니다. 구조체는 class와 동일하게 정의됩니다. 방금 언급한 제한말고 또 다른 점은 struct라는 키워드를 사용한다는 것입니다. 나머지는 동일합니다. struct는 멤버 변수 및 함수를 가질 수 있고, property와 연산자(operator)를 가질 수 있습니다. 아래의 코드는 단순한 struct를 잘 보여줍니다.


struct Point
{
  private int x, y;             // private fields

  public Point (int x, int y)   // constructor
  {
        this.x = x;
        this.y = y;
  }

  public int X                  // property
  {
        get {return x;}
        set {x = value;}
  }

  public int Y
  {
        get {return y;}
        set {y = value;}
  }
}    

Value Type과 Reference Types

앞에서 간단히 구조체와 클래스의 차이에 대해서 알아보았습니다. 이 차이에 대해서 좀더 살펴보려고 합니다. 사실 지금 언급하는 것이 가장 중요한 차이일 것입니다. 구조체는 value type입니다. 반면에 클래스는 reference type입니다. 프로그램이 실행되면 이 둘은 다르게 처리됩니다. value type instance는 생성되고, 값을 저장하기 위해서 메모리의 단일 공간에 할당됩니다. int, float, bool, char와 같은 기본(primitive) type도 value type이고 구조체와 같은 방식으로 처리됩니다. 프로그램이 실행되어 value type을 처리할 때, 기본적인 데이터를 직접 처리하기 때문에 매우 효과적입니다.


하지만 reference type object가 메모리에 생성될 때에는 포인터와 같이 분리된 참조로 제어합니다. 다음 코드의 Point는 구조체이며 Form은 클래스라고 가정하겠습니다. 그리고 Point와 Form을 각각 하나씩 생성하겠습니다.


Point p1 = new Point();         // Point는 구조체
Form f1 = new Form();           // Form은 클래스


이 경우 p1을 위한 Point가 단일 메모리 공간에 할당됩니다. 반면에 f1을 위한 Form은 두 공간에 나누어져 할당됩니다. 하나는 Form 자체를 위한 공간이고, 다른 하나는 참조 f1을 위한 공간입니다. 지금부터 이 차이에 대해서 알아보겠습니다.


Form f1;                        // Allocate the reference
f1 = new Form();                // Allocate the object


이제 다음과 같이 객체를 새로운 변수에 복사합니다.


Point p2 = p1;
Form f2 = f1;


구조체 p2는 독립적인 p1 복사본이 됩니다. 이 독립적인 복사본은 분리된 자신의 field를 가집니다. 그래서 p1과 p2는 각각의 객체를 가리킵니다. 하지만 f2는 reference만 복사됩니다. 결과적으로 f1과 f2는 동일한 객체를 가리키고 있습니다.


메소드(method)에 매개변수를 전달할 때에는 이런 C#의 특성을 더 주목해야합니다. 매개변수는 (기본적으로) value로 전달됩니다. value로 전달된 매개변수는 묵시적으로 복사되는 것입니다. value-type 매개변수는 (p2의 경우와 같이) 메모리를 실제로 복사합니다. 반면에 reference-type 매개변수는 (f2의 경우와 같이) reference만 복사합니다. reference가 가리키는 메모리 공간을 복사하지 않습니다. 다음 코드를 살펴보겠습니다.


Point myPoint = new Point (0, 0);      // value-type
Form myForm = new Form();              // reference-type
Test (myPoint, myForm);                

void Test (Point p, Form f)
{
     p.X = 100;                 // myPoint에 영향없음. p는 myPoint의 복사본
     f.Text = "Hello, World!";  // f는 myForm과 동일 대상 가리킴.
                               
     f = null;                  // f만 null, myForm은 여전히 유효
}


앞에서 언급하였듯이 value type을 매개변수로 전달하면 복사본을 생성합니다. Test() 함수의 p 매개변수는 myPoint의 복사본을 메모리에 생성하고, 그 복사본을 가리킵니다. 반면에 f 매개변수는 reference type이므로 myForm이 가리키는 instance을 복사하지 않고 참조인 myForm만 복사합니다. value type과는 다르게 reference 자체만 복사된다는 점에 주목합시다.


Test() 함수 내부에서 벌어지는 일을 더 자세히 살펴보겠습니다. 매개변수 p가 가리키는 대상은 myPoint가 가리키는 것과는 다른 복사본입니다. 그래서 p.X에 다른 값을 넣더라도 myPoint에는 영향을 주지 않습니다. 매개변수 f는 참조를 복사한 것이므로 f가 가리키는 대상은 myForm이 가리키는 것과 동일합니다. 그래서 f.Text 값을 변경하면 myForm.Text이 수정됩니다. 그런데 f에 null을 대입하면 myForm에는 아무런 영향을 미치지 않습니다. myForm도 null이 될 것으로 착각할 수도 있습니다. 하지만 f는 myForm이 가리키는 메모리 공간에 대한 참조입니다. f에 null을 대입하면 f는 더이상 이 공간을 가리키지 않습니다. 그러므로 myForm에는 영향을 줄 수 없습니다. myForm은 여전히 instance를 유효하게 가리키고 있습니다.


함수의 매개변수를 ref 한정어(modifier)를 사용해서 설정할 수 있습니다. “ref”로 전달하면 메소드는 호출자(caller)의 인자에 직접 접근할 수 있습니다. 아래의 예시 코드를 살펴보겠습니다.


Point myPoint = new Point (0, 0);      // value-type
Form myForm = new Form();              // reference-type
Test (ref myPoint, ref myForm);        // 참조로 전달

void Test (ref Point p, ref Form f)
{
     p.X = 100;                       // myPoint.X 수정
     f.Text = "Hello, World!";        // myForm.Text 수정
     f = null;                        // myForm에 null을 대입!
}


Test() 함수의 매개변수는 모두 ref로 설정되어 있습니다. 만약 우리가 인자를 ref로 전달하면 인자의 type에 상관없이 모두 원본 instance에 접근하여 수정할 수 있습니다.

다음에 다룰 내용은 무엇인가요?

이번 글에서는 C#에서 value-type과 reference-type을 처리하는 과정이 어떻게 다른지 간단하게 살펴보았습니다. 다음에는 이 차이점을 메모리 관점에 좀더 상세하게 살펴보고자 합니다. value-type과 reference-type이 스택(stack)과 힙(heap)에 어떻게 할당되고 해제되는지 알아볼 것입니다. 이를 이해하면 C#의 가비지 콜렉션(garbage collection)은 쉽게 이해될 것입니다.

댓글 2개:

  1. 안녕하세요 ^_^
    본 포스팅 목적과 별개로 갖게된 궁금증에 대해 얘기를 나눠보고 싶어서 글 남깁니다.
    위 구조체 설명주신 부분에서 Point에 대한 코드를 작성되있는데,
    x, y라는 변수가 있고 그것을 게터세터를 통해 접근 가능하도록 짜여있습니다.
    이 상황에서 x, y변수를 굳이 private으로 만들고 게터세터를 이용한다는 점이 가지는 이득이 있을까요? 이득이란 어떠한 상황에서라도 가질 수 있는 이득을 말합니다. 사소한거라도!

    지적이나 비꼬는 것은 절대 아니니 편하게 생각해주셨으면 좋겠어요.
    문득 아무렇지 않게 사용하던 게터세터가 저런 상황에 쓰임의 의미가 있는 것일까? 라는 의문을 가지게되어 얘기를 꺼내봅니다!

    답글삭제
    답글
    1. http://www.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS5678358974
      검색하면바로나오네요^^

      삭제