[C#] 10. 인스턴스 생성(new)과 메모리 할당(Stack 메모리와 Heap 메모리) 그리고 null


Study/C#  2020. 7. 7. 16:24

안녕하세요. 명월입니다.


이 글은 C#에서 인스턴스 생성(new)과 메모리 할당(Stack 메모리와 Heap 메모리) 그리고 null에 대한 글입니다.


이전에 제가 C#에서 클래스를 생성하는 방법에 대해 설명한 적이 있습니다.

링크 - [C#] 09. 클래스 생성하는 방법(생성자, 소멸자)


클래스를 만들고 인스턴스를 생성하는 키워드는 new입니다. 인스턴스 생성이란 클래스를 메모리에 등록하는 것과 같습니다.

using System;
namespace Example
{
  // Exam 클래스
  class Example
  {
    // 맴버 변수
    private int data;
    // 생성자
    public Example(int data)
    {
      // 맴버 변수 설정
      this.data = data;
    }
    // 함수
    public void Print()
    {
      // 콘솔 출력
      Console.WriteLine("Data - " + data);
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      Example ex = new Example(10);
      ex.Print();

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

여기서 Example ex = new Example(int)의 뜻은 Example 타입의 ex 변수에 new Example으로 인스턴스를 생성해서 넣고 Example(int) 으로 int형이 파라미터를 가진 생성자를 호출하는 뜻이 됩니다.

먼저, 살펴봐야 할 것은 변수 선언입니다. 이전에 변수 선언에 대해서 설명한 적이 있습니다만 이 변수 선언은 타입과 값이 일치해야 합니다. 즉, int형에 실수형을 넣거나 문자열을 넣을 수 없는 것처럼 Example 타입에는 반드시 Example 인스턴스가 들어가야 합니다.

이 Example ex는 할당된 메모리 주소를 가르키는 것으로 일명 포인트 변수라고 합니다. 즉, 변수에 값이 들어가는 것이 아니고 메모리 주소의 값이 들어가는 것입니다.

위의 이미지같은 구조가 되는 것입니다.

여기서 Stack 메모리와 Heap 메모리에 대한 구조가 나왔습니다.

잠깐 이 Stack 메모리와 Heap 메모리에 대해 이해할 필요가 있습니다.

Stack 메모리는 우리가 프로그램에서 함수를 작성할 때, 실행하는 영역을 중괄호({ })로 설정합니다. 이 중괄호의 영역을 우리는 Stack 영역이라고 이야기합니다. 이 Stack 영역에서 선언된 변수의 값은 우리가 Stack 메모리에 보관 된다고 합니다.

위 이미지를 보시면 Main 함수 안에 임의의 중괄호를 사용해서 새로운 스택 영역을 만들었습니다. 그 새로운 스택 영역에서 int형 data를 만들었습니다만 스택 영역을 벗어나서 사용하려고 하면 존재하지 않는 변수라고 에러가 나옵니다.

다시 돌아와서 Example ex는 Main 함수의 스택 영역에서 선언된 것입니다.


new Example(10)는 Heap 영역에서 할당된 것인데, Heap은 프로그램의 영역의 메모리 구조입니다. 즉, 프로그램이 실행되면서 Heap 메모리 영역이 생성이 되고 그 안에서 자유롭게 선언하고 해지를 할 수 있습니다.

Heap 메모리에 인스턴스 할당은 new 키워드로 할당하고 GC(가비지 컬렉션)으로 메모리 해제가 이루어 집니다. Stack 영역은 Stack 알고리즘으로 그 위치를 찾을 수 있지만 Heap의 경우는 주소값이 없으면 그 위치를 찾을 수 없습니다.


정리하면 Stack은 정적인 메모리 영역이며 데이터를 찾기가 쉽지만(Stack 알고리즘의 push pop으로 데이터를 찾는다.), Heap은 동적인 메모리 영역이며 new키워드로 클래스를 할당하면 데이터를 주소 값으로만 찾을 수 있습니다.

그리고 이 둘을 연결한 게 Example ex = new Example(10);의 형태입니다.


그럼 왜 C#에서는 이렇게 복잡하게 Stack = Heap의 구조로 데이터를 취급하는 것일까?

이 개념은 사실 C/C++에서 온 개념이긴 합니다. C/C++에서는 new를 사용하지 않고 클래스를 Stack 영역에 인스턴스를 선언할 수 있습니다. 원시 데이터 타입(Primitive type)처럼 말입니다.

그러나 이 Stack 메모리가 무한정 정해져 있는게 아니고 Stack의 메모리가 정해져 있습니다. 보통은 이 크기가 한 1MB정도? 2MB정도 사용합니다.

포인트 값의 메모리 크기는 int형으로 4byte정도 되는데 약 25만개 정도 선언할 수 있는 크기입니다. 25만개라고 하면 큰거 같지만, 이게 사실 금방입니다.


그리고 Stack 메모리에서는 꼭 이 변수 할당만 쓰는게 아니고 함수 상태(Interrupt)등 사용하는 데이터가 많이 있습니다. 그러면 이 Stack 메모리라는게 많은 양이 아닙니다.

그리고 최근 프로그램 실행하면 기본 몇 GB도 우습게 사용하는데 1MB는 엄청 적은 것입니다. 그렇게 이 Stack에 할당되어 있는 메모리를 전부 사용하면 그 유명한 StackOverflow 에러가 발생합니다.

그럼 이 Stack 메모리 설정을 높이면 되지 않을까 생각하는데.. 맞습니다. Stack 메모리를 높이면 StackOverflow는 해결됩니다. 그러나 이 Stack 메모리는 구조가 Stack 알고리즘 구조로 push와 pop으로 데이터를 찾는 건데, 당연히 메모리가 커지면 느려집니다. 꼭, Stack이 아니더라도 탐색 알고리즘으로 되어 있는 자료 구조는 커지면 느려집니다. 즉, Stack 메모리가 많아지면 프로그램은 느려집니다.

Heap은 이렇게 Stack처럼 정형화 된 자료 구조가 아니기 때문에 용량이 큽니다. 그러나 이를 참조하기 위해선 반드시 메모리 주소 값이 있어야 하는데, 이 메모리 주소 값을 Stack 영역에 보관을 하는 것입니다.

C#에서 이 주소 값을 확인할 수 있는 GetHashCode 함수가 있습니다.

using System;
namespace Example
{
  // Exam 클래스
  class Example
  {
    // 맴버 변수
    private int data;
    // 생성자
    public Example(int data)
    {
      // 맴버 변수 설정
      this.data = data;
    }
    // 맴버 변수 재설정 함수
    public void setData(int data)
    {
      this.data = data;
    }
    // 함수
    public void Print()
    {
      // 콘솔 출력
      Console.WriteLine("Data - " + data);
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      Example ex1 = new Example(10);
      Example ex2 = ex1;

      // ex1 변수의 메모리 주소값
      Console.WriteLine(ex1.GetHashCode());
      // ex2 변수의 메모리 주소값
      Console.WriteLine(ex2.GetHashCode());
      // ex2 변수의 맴버 변수 재설정
      ex2.setData(20);
      // ex1의 맴버 변수값
      Console.WriteLine("ex1 -");
      ex1.Print();
      // ex2의 맴버 변수값
      Console.WriteLine("ex2 -");
      ex2.Print();

      // 아무 키나 누르시면 종료합니다.
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

위 소스를 보시면 ex1에는 new로 클래스를 할당했습니다. 그리고 ex2의 변수에 ex1를 넣었습니다.

GetHashCode를 보니 값은 값이 나오네요. 이는 같은 클래스임을 설명하는 것입니다.

그리고 ex2의 data를 수정했습니다.

결과는 ex1과 ex2이 같은 값이 나옵니다.

즉, 위와 같은 형태로 메모리가 묶여 있는 것입니다. ex1변수와 ex2의 변수는 heap의 같은 클래스를 가르키고 있기 때문에 ex2의 값을 수정해도 ex1에 영향이 가는 것입니다.


여기서 Example ex에 new Example의 인스턴스를 생성해서 넣었습니다.

그렇다면 new Example를 사용하지 않는 경우도 있습니다.


이런 경우는 프로그램에서 null이라고 하는 것입니다. null이라는 것은 값이 비었다라는 뜻이 맞기는 하는데 정확하게 ex의 변수에 할당된 인스턴스가 연결된 것이 없는 것입니다.

null이라는 것도 결국에는 하나의 값으로 정확히는 ex 변수에 연결된 인스턴스가 없다라는 뜻입니다.

그리고 null로 설정된 변수를 호출하게 되면 null exception이 발생하게 됩니다.


여기까지 C#에서 인스턴스 생성(new)과 메모리 할당(Stack 메모리와 Heap 메모리) 그리고 null에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.