---
title: "new 는 힙에 자리를 만들고 주소를 돌려준다"
description: "변수 안에 객체가 들어 있는 것이 아닙니다. 객체가 있는 곳의 주소가 들어 있습니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초", "메모리"]
canonical: https://blog.unialgames.com/posts/csharp-new-and-heap/
---
```csharp
Dog a = new Dog();
```

이 한 줄에서 무슨 일이 일어나는지 봅니다. 여기가 앞으로 나올 이야기의 대부분을 설명합니다.

앞에서 변수는 값을 담는 자리라고 했습니다. `int` 하나면 4바이트 자리가 생기고 거기에 숫자가 들어갔습니다.
그럼 `Dog` 는 어떨까요. `Dog` 는 몇 바이트일까요.

`Name` 은 문자열이라 길이가 정해져 있지 않습니다.
상속을 하면 필드가 더 붙기도 합니다. 미리 정하기 어렵습니다.

> **그래서 객체는 다른 곳에 만들고, 변수에는 그 위치만 담습니다.**

## 메모리는 두 군데로 나뉘어 있다

프로그램이 쓰는 메모리는 크게 두 덩어리입니다.

| | 스택 | 힙 |
| --- | --- | --- |
| 무엇이 놓이나 | 지역 변수, 매개변수 | `new` 로 만든 객체 |
| 크기 | 작다 | 크다 |
| 언제 사라지나 | 그 블록을 벗어나면 바로 | 아무도 안 가리키게 되면 나중에 |
| 누가 치우나 | 자동 | 가비지 컬렉터(GC) |
| 속도 | 빠르다 | 상대적으로 느리다 |

**스택**은 앞에서 이미 만났습니다. 메서드를 부르면 자리가 쌓이고 끝나면 사라지던 그것입니다.
지역 변수도 거기 놓입니다. 그래서 메서드가 끝나면 지역 변수가 같이 사라집니다.
치우는 규칙이 단순해서 빠릅니다. 대신 자리가 작습니다. 재귀가 깊어지면 넘치던 이유입니다.

**힙**은 크기와 수명을 미리 못 정하는 것들을 놓는 곳입니다.
`new` 로 만든 객체가 여기 갑니다.

## new 가 하는 일

```csharp
Dog a = new Dog();
```

세 가지가 순서대로 일어납니다.

1. 힙에 `Dog` 하나가 들어갈 자리를 잡고 기본값으로 채웁니다.
2. 그 자리의 **주소**를 돌려줍니다.
3. 그 주소를 스택의 변수 `a` 에 담습니다.

<x-heap>
<script type="application/json">
{
  "label": "new 가 하는 일",
  "steps": [
    {
      "code": "int n = 10;",
      "why": "int 는 값을 직접 담습니다. 스택 칸 안에 10 이 그대로 들어갑니다.",
      "stack": [{ "name": "n", "value": "10", "hot": true }],
      "heap": []
    },
    {
      "code": "Dog a = new Dog();",
      "why": "힙에 자리를 잡습니다. 아직 아무것도 안 넣었으니 기본값입니다.",
      "stack": [{ "name": "n", "value": "10" }, { "name": "a", "ref": 0, "hot": true }],
      "heap": [{ "id": 0, "title": "Dog", "fields": ["Name: null", "Age: 0"], "hot": true }]
    },
    {
      "code": "a.Name = \"바둑\";",
      "why": "a 를 따라가서 그 자리의 Name 을 채웁니다. 바뀌는 것은 힙 쪽입니다.",
      "stack": [{ "name": "n", "value": "10" }, { "name": "a", "ref": 0 }],
      "heap": [{ "id": 0, "title": "Dog", "fields": ["Name: \"바둑\"", "Age: 0"], "hot": true }]
    },
    {
      "code": "Dog b = new Dog();",
      "why": "또 부르면 힙에 자리가 하나 더 생깁니다. 번호가 다르니 다른 상자입니다.",
      "stack": [{ "name": "n", "value": "10" }, { "name": "a", "ref": 0 }, { "name": "b", "ref": 1, "hot": true }],
      "heap": [
        { "id": 0, "title": "Dog", "fields": ["Name: \"바둑\"", "Age: 0"] },
        { "id": 1, "title": "Dog", "fields": ["Name: null", "Age: 0"], "hot": true }
      ]
    }
  ],
  "note": "스택 칸에 든 것은 객체가 아니라 힙 상자를 가리키는 표입니다. 실제로는 주소 숫자가 들어 있습니다."
}
</script>
</x-heap>

**변수 `a` 안에 개가 들어 있는 것이 아닙니다.** 개가 있는 곳의 주소가 들어 있습니다.

이렇게 주소를 담는 타입을 **참조 타입** 이라고 합니다.
`int` 처럼 값을 직접 담는 타입은 **값 타입** 입니다.

클래스는 전부 참조 타입입니다. 배열도, `string` 도, `List` 도 그렇습니다.
앞에서 `List` 를 메서드에 넘겼을 때 밖이 같이 바뀌던 이유가 이것입니다.
복사된 것이 목록이 아니라 주소였습니다.

## 기본값은 null 이다

`new` 로 잡은 자리는 기본값으로 채워집니다. 배열을 만들 때와 같습니다.
숫자는 0, `bool` 은 `false` 입니다.

참조 타입 필드는 **`null`** 입니다. `null` 은 "아무것도 안 가리킴" 이라는 뜻입니다.
값이 0인 것과 다릅니다. **가리키는 대상이 없는 상태**입니다.

```csharp
Dog a = new Dog();
Console.WriteLine(a.Name.Length);    // NullReferenceException
```

`a.Name` 이 `null` 인데 그 안의 `Length` 를 물었습니다.
아무것도 없는 주소로 찾아간 것이라 그 자리에서 죽습니다.

```csharp
Dog a = null;      // 변수 자체가 아무것도 안 가리킬 수도 있습니다
a.Bark();          // NullReferenceException
```

이 예외는 C# 을 쓰는 내내 만납니다. 원인은 언제나 같습니다.
**아무도 안 가리키는 것을 따라간 것**입니다.

## 아무도 안 가리키면 치워진다

```csharp
Dog a = new Dog();
a.Name = "바둑";

a = new Dog();      // 다른 것을 가리키게 했습니다
```

처음 만든 개는 어떻게 될까요. `a` 가 다른 곳을 가리키게 됐으니
**아무도 그 상자를 가리키지 않습니다.** 주소를 잃어버렸으니 다시 찾아갈 방법도 없습니다.

C# 에서는 이런 상자를 직접 치우지 않습니다.
가비지 컬렉터가 가끔 돌면서 **아무도 안 가리키는 것**을 찾아 치웁니다.

<x-heap>
<script type="application/json">
{
  "label": "가리키지 않으면 버려진다",
  "steps": [
    {
      "code": "Dog a = new Dog(); a.Name = \"바둑\";",
      "why": "a 가 0번 상자를 가리킵니다.",
      "stack": [{ "name": "a", "ref": 0 }],
      "heap": [{ "id": 0, "title": "Dog", "fields": ["Name: \"바둑\""] }]
    },
    {
      "code": "a = new Dog();",
      "why": "새 상자를 만들고 a 가 그쪽을 가리킵니다. 0번은 아무도 안 가리킵니다.",
      "stack": [{ "name": "a", "ref": 1, "hot": true }],
      "heap": [
        { "id": 0, "title": "Dog", "fields": ["Name: \"바둑\""], "dead": true },
        { "id": 1, "title": "Dog", "fields": ["Name: null"], "hot": true }
      ]
    },
    {
      "code": "// 언젠가 GC 가 돌면",
      "why": "가비지 컬렉터가 아무도 안 가리키는 상자를 치웁니다. 언제 도는지는 우리가 정하지 않습니다.",
      "stack": [{ "name": "a", "ref": 1 }],
      "heap": [{ "id": 1, "title": "Dog", "fields": ["Name: null"] }]
    }
  ],
  "note": "흐린 상자는 아직 메모리에 남아 있지만 다시 쓸 수 없는 상태입니다."
}
</script>
</x-heap>

편하지만 공짜는 아닙니다. GC 가 도는 동안 프로그램이 잠깐 멈춥니다.
평소에는 신경 쓸 일이 없는데, 게임처럼 매 프레임이 중요한 곳에서는
이 멈춤이 화면 끊김으로 보입니다.

그래서 반복문 안에서 `new` 를 계속 부르면 곤란합니다.
쓰레기를 계속 만들어 내는 것이니까요.
같은 객체를 다시 쓰는 방식이 그래서 나옵니다.

## 정리

- 메모리는 스택과 힙으로 나뉩니다. 지역 변수는 스택, `new` 로 만든 객체는 힙에 놓입니다.
- 스택은 블록을 벗어나면 바로 치워집니다. 빠르지만 작습니다.
- 힙은 크기와 수명을 미리 못 정하는 것들이 놓입니다. GC 가 치웁니다.
- `new` 는 힙에 자리를 잡고 그 **주소**를 돌려줍니다. 변수에는 주소가 담깁니다.
- 그래서 클래스는 참조 타입입니다. `int` 같은 값 타입은 값을 직접 담습니다.
- 참조 타입의 기본값은 `null` 입니다. 그걸 따라가면 `NullReferenceException` 입니다.
- 아무도 안 가리키는 객체는 GC 가 치웁니다. 도는 동안 잠깐 멈춥니다.