---
title: "대입하면 값이 복사되거나 주소가 복사된다"
description: "규칙은 하나입니다. 담긴 것이 복사됩니다. 담긴 것이 값이냐 주소냐에 따라 결과가 완전히 달라집니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초", "메모리"]
canonical: https://blog.unialgames.com/posts/csharp-value-vs-reference/
---
두 코드를 비교해 봅시다.

```csharp
int x = 10;
int y = x;
y = 20;

Console.WriteLine(x);      // 10
```

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

Console.WriteLine(a.Name); // 초코
```

같은 모양인데 결과가 반대입니다. `x` 는 안 바뀌고 `a.Name` 은 바뀌었습니다.

규칙이 다른 것이 아닙니다. **규칙은 하나입니다. 담긴 것이 복사됩니다.**
담긴 것이 무엇이냐가 다를 뿐입니다.

## 담긴 것이 값이면 값이 복사된다

`int` 는 값 타입입니다. 변수 안에 숫자가 그대로 들어 있습니다.
`y = x` 는 그 숫자를 옮겨 적습니다. 자리가 둘이 됩니다.

<x-heap>
<script type="application/json">
{
  "label": "값 타입을 대입하면",
  "steps": [
    {
      "code": "int x = 10;",
      "why": "스택에 자리가 생기고 10 이 들어갑니다.",
      "stack": [{ "name": "x", "value": "10", "hot": true }],
      "heap": []
    },
    {
      "code": "int y = x;",
      "why": "x 에 든 값 10 을 옮겨 적습니다. 자리가 둘이 됐습니다.",
      "stack": [{ "name": "x", "value": "10" }, { "name": "y", "value": "10", "hot": true }],
      "heap": []
    },
    {
      "code": "y = 20;",
      "why": "y 의 자리를 고칩니다. x 는 다른 자리라 모릅니다.",
      "stack": [{ "name": "x", "value": "10" }, { "name": "y", "value": "20", "hot": true }],
      "heap": []
    }
  ],
  "note": "메서드에 값을 넘길 때와 같은 일입니다. 그때도 자리가 둘이었습니다."
}
</script>
</x-heap>

## 담긴 것이 주소면 주소가 복사된다

`Dog` 는 참조 타입입니다. 변수 안에 든 것은 개가 아니라 **개가 있는 곳의 주소**입니다.
`b = a` 는 그 주소를 옮겨 적습니다.

주소를 복사했으니 **둘이 같은 곳을 가리킵니다.** 개는 여전히 한 마리입니다.

<x-heap>
<script type="application/json">
{
  "label": "참조 타입을 대입하면",
  "steps": [
    {
      "code": "Dog a = new Dog(\"바둑\");",
      "why": "힙에 개 한 마리가 생기고, a 에는 그 자리의 주소가 담깁니다.",
      "stack": [{ "name": "a", "ref": 0, "hot": true }],
      "heap": [{ "id": 0, "title": "Dog", "fields": ["Name: \"바둑\""], "hot": true }]
    },
    {
      "code": "Dog b = a;",
      "why": "복사되는 것은 주소입니다. 개가 한 마리 더 생기지 않습니다.",
      "stack": [{ "name": "a", "ref": 0 }, { "name": "b", "ref": 0, "hot": true }],
      "heap": [{ "id": 0, "title": "Dog", "fields": ["Name: \"바둑\""] }]
    },
    {
      "code": "b.Name = \"초코\";",
      "why": "b 를 따라가서 이름을 바꿉니다. a 도 같은 곳을 가리키고 있습니다.",
      "stack": [{ "name": "a", "ref": 0 }, { "name": "b", "ref": 0 }],
      "heap": [{ "id": 0, "title": "Dog", "fields": ["Name: \"초코\""], "hot": true }]
    },
    {
      "code": "b = new Dog(\"흰둥\");",
      "why": "이건 다릅니다. b 가 다른 주소를 담게 된 것뿐이라 a 는 그대로입니다.",
      "stack": [{ "name": "a", "ref": 0 }, { "name": "b", "ref": 1, "hot": true }],
      "heap": [
        { "id": 0, "title": "Dog", "fields": ["Name: \"초코\""] },
        { "id": 1, "title": "Dog", "fields": ["Name: \"흰둥\""], "hot": true }
      ]
    }
  ],
  "note": "가리키는 것의 내용을 바꾸면 둘 다 보이고, 가리키는 대상을 바꾸면 자기만 바뀝니다."
}
</script>
</x-heap>

마지막 단계가 헷갈리는 자리입니다. 두 가지를 구분해야 합니다.

| 하는 일 | 무엇이 바뀌나 | 다른 변수에 보이나 |
| --- | --- | --- |
| `b.Name = "초코"` | 가리키는 **대상의 내용** | 보인다 |
| `b = new Dog(...)` | `b` 가 **가리키는 곳** | 안 보인다 |

앞에서 메서드에 `List` 를 넘기면 밖이 바뀐다고만 하고 넘어간 자리가 있습니다.
이유가 이것입니다. 복사된 것이 목록이 아니라 주소였습니다.
`list.Add(99)` 는 가리키는 대상을 고친 것이라 밖에 보이고,
`list = new List<int>()` 는 가리키는 곳을 바꾼 것이라 안 보입니다.

## 어떤 것이 값이고 어떤 것이 참조인가

| | 값 타입 | 참조 타입 |
| --- | --- | --- |
| 무엇이 있나 | `int`, `double`, `bool`, `char`, `enum`, `struct` | `class`, 배열, `string`, `List` 같은 것들 |
| 변수에 담긴 것 | 값 자체 | 주소 |
| 어디에 놓이나 | 보통 스택 | 힙 (변수는 스택) |
| 대입하면 | 값이 복사된다 | 주소가 복사된다 |
| `null` 이 될 수 있나 | 안 된다 | 된다 |

`string` 이 참조 타입이라는 것이 걸릴 수 있습니다.
앞에서 문자열은 바꿀 수 없다고 했는데, 그래서 값처럼 행동합니다.

```csharp
string s1 = "안녕";
string s2 = s1;
s2 = "잘가";           // s2 가 다른 문자열을 가리키게 된 것

Console.WriteLine(s1); // 안녕
```

주소가 복사되긴 했는데, **가리키는 대상을 고칠 방법이 없으니** 문제가 안 생깁니다.
문자열을 불변으로 만든 이유 중 하나가 이것입니다.

## struct 는 값 타입이다

클래스와 거의 같게 생겼는데 값 타입인 것이 있습니다. `struct` 입니다.

```csharp
struct Point
{
    public int X;
    public int Y;
}

Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1;
p2.X = 99;

Console.WriteLine(p1.X);    // 1, 안 바뀝니다
```

`class` 를 `struct` 로 바꾸기만 했는데 결과가 반대가 됐습니다.
`struct` 는 변수 안에 내용이 통째로 들어 있어서, 대입하면 **내용 전체가 복사**됩니다.

| | `class` | `struct` |
| --- | --- | --- |
| 타입 | 참조 | 값 |
| 대입하면 | 주소가 복사 | 내용이 통째로 복사 |
| `null` | 된다 | 안 된다 |
| 상속 | 된다 | 안 된다 |

**언제 `struct` 를 쓰나.** 작고, 값처럼 다루는 것이 자연스러운 묶음일 때입니다.
좌표, 색상, 크기 같은 것들입니다. `(3, 4)` 라는 좌표는 그 자체가 값이지
"어떤 좌표 객체" 가 아닙니다.

크면 쓰면 안 됩니다. 대입할 때마다 통째로 복사되니 필드가 많으면 그게 부담입니다.
**애매하면 `class` 를 씁니다.** 실무에서 `struct` 를 직접 만들 일은 드뭅니다.

## 박싱은 값을 힙으로 옮겨 담는 것이다

값 타입을 참조 타입 자리에 넣으면 문제가 생깁니다. 값은 스택에 있는데
참조 타입 자리는 주소를 기대하기 때문입니다.

C# 은 이때 **힙에 상자를 하나 만들고 값을 거기 넣습니다.** 이걸 박싱이라고 합니다.

```csharp
int n = 42;
object o = n;        // 박싱: 힙에 상자를 만들어 42 를 넣는다
int m = (int)o;      // 언박싱: 다시 꺼낸다
```

`object` 는 모든 타입의 부모입니다. 무엇이든 받을 수 있는 자리인데,
참조 타입이라 값을 그대로는 못 담습니다.

문제는 비용입니다. **박싱은 `new` 를 부르는 것과 같습니다.**
힙에 자리를 잡고, 나중에 GC 가 치웁니다.

```csharp
for (int i = 0; i < 100000; i++)
{
    object o = i;    // 10만 번 박싱, 10만 개의 쓰레기
}
```

반복문 안에서 일어나면 눈에 띄게 느려집니다.
이 문제를 해결하는 것이 제네릭인데, 그건 뒤에서 다룹니다.
`List<int>` 의 꺾쇠가 바로 그것입니다.

## 정리

- 대입할 때 복사되는 것은 **변수에 담긴 것**입니다. 규칙은 하나입니다.
- 값 타입은 값이 담겨 있어서 값이 복사됩니다. 자리가 둘이 됩니다.
- 참조 타입은 주소가 담겨 있어서 주소가 복사됩니다. 둘이 같은 것을 가리킵니다.
- 가리키는 대상의 **내용**을 바꾸면 다른 변수에도 보입니다.
- 변수가 **가리키는 곳**을 바꾸면 자기만 바뀝니다.
- `string` 은 참조 타입이지만 고칠 수 없어서 값처럼 행동합니다.
- `struct` 는 값 타입입니다. 작고 값처럼 다룰 묶음에만 씁니다. 애매하면 `class` 입니다.
- 값 타입을 `object` 자리에 넣으면 박싱이 일어납니다. 힙에 상자가 생기는 일입니다.