---
title: "변수를 선언하면 무슨 일이 일어나는가"
description: "변수는 값을 담는 상자가 아니라 이름이 붙은 메모리 칸입니다. 선언·할당·사용이 각각 언제 무엇을 하는지 그림으로 봅니다."
author: "유니얼"
pubDate: 2026-09-08
updatedDate: 2026-10-26
category: "programming/csharp/basics"
tags: ["C#", "기초", "메모리"]
canonical: https://blog.unialgames.com/posts/csharp-variable-and-memory/
---
변수를 처음 배울 때 대부분 "값을 담는 상자"라고 배웁니다. 틀린 말은 아닌데,
이 설명으로는 뒤에 나오는 것들이 하나도 설명되지 않습니다.
`int` 는 왜 21억까지만 되는지, 선언만 하고 읽으면 왜 컴파일 에러인지,
`0.1 + 0.2` 는 왜 `0.3` 이 아닌지. 전부 따로 외워야 할 규칙이 됩니다.

정확하게 말하면 이렇습니다.

> **변수는 메모리의 한 자리에 붙인 이름입니다.
> 그 자리가 어디고 몇 바이트인지는 타입이 정합니다.**

이 문장 하나면 위의 셋이 전부 같은 이유로 설명됩니다. 하나씩 그림으로 봅니다.

## 이름은 사람만 쓰는 것이다

우리가 쓴 C# 코드는 바로 실행되지 않습니다. 먼저 **컴파일러**라는 프로그램이 코드를 CPU 가 알아듣는 명령(기계어)으로 바꾸고,
그다음에 실제로 돌아갑니다. 코드를 바꾸는 때를 **컴파일 시점**, 프로그램이 돌아가는 때를 **실행 시점**이라고 부릅니다.

`int age = 25;` 라고 쓰면 컴파일러는 두 가지를 합니다.

1. `age` 라는 이름에 **메모리 한 자리를 배정합니다.** 지역 변수(메서드의 `{ }` 안에서 선언한 변수)라면
   그 메서드의 스택 프레임 안 몇 번째 바이트인지가 여기서 정해집니다.
   스택 프레임은 메서드 하나가 실행되는 동안 지역 변수를 두려고 잡아 두는 메모리 구역입니다.
   메서드가 끝나면 통째로 돌려줍니다.
2. 그 자리의 **크기를 4바이트로 잡습니다.** `int` 가 하는 일이 이겁니다.

여기서 중요한 게 하나 있습니다. **`age` 라는 이름은 컴파일이 끝나면 사라집니다.**
기계어에는 이름이 없고 "스택 프레임 시작에서 몇 바이트 떨어진 자리" 만 남습니다.
이름은 사람이 읽으려고 붙인 것이지, CPU 가 쓰는 게 아닙니다.

CPU 는 계산만 합니다. 값은 전부 메모리에 있고, CPU 는 **자리로** 찾아가서 읽고 씁니다.

### 그럼 이름은 왜 필요한가

계산한 값을 기억해 두었다가 나중에 다시 쓰거나 바꾸려면 그 값이 놓인 자리를 가리킬 방법이 있어야 합니다.
이름이 없으면 "스택 프레임 시작에서 12바이트 떨어진 곳" 같은 위치를 사람이 직접 기억해야 합니다.
**변수는 그 위치를 컴파일러가 대신 관리하게 하고, 우리는 `age` 같은 이름으로만 부르게 해 줍니다.**

## 선언 · 할당 · 사용은 시점부터 다르다

한 줄에 같이 쓰다 보니 하나로 보이지만, 셋은 **일어나는 시점부터** 다릅니다.
타입을 바꿔가며 보세요. 잡히는 칸 수가 그대로 바뀝니다.

<x-mem>
<script type="application/json">
{
  "label": "변수를 선언하면 일어나는 일",
  "name": "age",
  "value": 25,
  "default": 4,
  "baseAddr": 4096,
  "note": "칸 하나가 1바이트입니다. 칸 안의 두 글자는 그 바이트에 실제로 들어가는 값을 16진수(0~9 와 A~F 로 한 자리에 16가지를 쓰는 표기)로 적은 것이고, 낮은 주소에 낮은 자리가 갑니다(리틀 엔디언, x86·ARM 기준)."
}
</script>
</x-mem>

- **선언은 컴파일 시점**입니다. "이 자리는 `age` 것" 이라고 정해질 뿐,
  실행 중에 아무 일도 일어나지 않습니다.
- **할당은 실행 시점**입니다. 그 자리에 비트를 씁니다.
  `=` 는 "같다" 가 아니라 **"오른쪽을 계산해서 왼쪽 자리에 써라"** 는 명령입니다.
- **사용도 실행 시점**입니다. 그 주소에서 4바이트를 읽어 옵니다.
  읽는다고 칸이 비지 않습니다.

> 실제로는 실행 직전에 코드를 기계어로 바꾸는 JIT 이 자주 쓰는 지역 변수를 메모리 대신
> CPU 안의 아주 작은 저장 칸(레지스터)에 올려두기도 하고,
> 그러면 이 칸이 아예 안 생길 수도 있습니다. 그래도 **"이름 하나에 자리 하나, 크기는 타입이 정한다"**
> 는 모델은 그대로 맞습니다. 이 글의 그림은 그 모델입니다.

## 선언만 하고 읽으면 왜 에러인가

위 그림에서 **선언 단계의 칸이 `00` 이 아니라 `??`** 인 걸 보세요. 이게 핵심입니다.

선언은 자리를 정할 뿐 **아무것도 쓰지 않습니다.** 그러니 그 자리에는
이전에 그 메모리를 쓰던 무언가가 남아 있습니다. 0일 수도 있고 아닐 수도 있습니다.

```csharp
int age;
Console.WriteLine(age);   // CS0165: 할당되지 않은 지역 변수 'age'를 사용했습니다
```

C# 은 이걸 **컴파일 단계에서** 막습니다. 실행해보고 이상하면 알려주는 게 아니라,
아예 빌드가 안 됩니다. 값이 뭐가 나올지 아무도 모르는 코드를 애초에 못 만들게 하는 겁니다.

> 이 규칙은 **지역 변수**에만 적용됩니다. 클래스 바로 안에 선언한 변수(필드)는 객체가 만들어질 때
> 프로그램을 실행해 주는 쪽(런타임)이 그 메모리를 전부 0으로 채워 주기 때문에 선언만 하고 읽어도 됩니다.
> 지역 변수는 스택 프레임 안에 잡히고, 그 자리는 아무도 청소해주지 않습니다.

## 크기가 범위를 정한다

**비트(bit)** 는 0 또는 1 하나입니다. 8개 모이면 **바이트(byte)** 입니다.

칸을 직접 눌러 보세요. 각 칸이 맡은 자릿값이 아래에 적혀 있습니다.
전부 켜면 255가 나오는데, 그게 `byte` 의 최댓값입니다.

<x-bits>
<script type="application/json">
{
  "label": "비트를 직접 켜보기",
  "bits": 8,
  "value": 65,
  "showChar": true
}
</script>
</x-bits>

**"자동으로 세기"** 를 눌러 두고 보세요. 255 다음에 256이 아니라 **0으로 돌아갑니다.**
자릿수가 8개로 정해진 이상 값은 돌 수밖에 없습니다. 시계의 12시 다음이 1시인 것과 같습니다.

여기서 알아야 할 건 숫자가 아니라 **어디서 나온 숫자인가** 입니다.

> `byte` 가 0~255 인 건 누가 정한 규칙이 아니라,
> **8비트로 만들 수 있는 조합이 2<sup>8</sup> = 256가지**라서 나오는 계산 결과입니다.

`int` 도 똑같습니다. 4바이트 = 32비트 = 2<sup>32</sup> = 약 42억 가지입니다.
`int` 는 그중 절반을 음수에 쓰기 때문에 약 -21억 ~ 21억이 됩니다. 처음에 물었던 "왜 21억까지만" 의 답이 이것입니다.
`int` 도 `byte` 처럼 최댓값(2,147,483,647)에 1을 더하면 넘친 만큼 돌아서 음수 끝으로 갑니다. 이것을 **오버플로**라고 부릅니다.

## 같은 비트가 65도 되고 'A'도 된다

위 그림에서 65를 만들어 두면 **문자로 읽으면** 칸에 `A` 가 떠 있습니다.
메모리에는 0과 1밖에 없고, 그걸 **무엇으로 읽을지는 타입이 정합니다.**

<x-compare>
<script type="application/json">
{
  "label": "같은 비트, 다른 타입",
  "tabs": [
    { "label": "int 로 읽으면", "code": "int n = 65;\nConsole.WriteLine(n);   // 65", "stats": [["메모리 크기", "4바이트"], ["값을 이루는 비트", "0100 0001"], ["타입의 해석", "숫자 65"]] },
    { "label": "char 로 읽으면", "code": "char c = (char)65;\nConsole.WriteLine(c);   // A", "stats": [["메모리 크기", "2바이트"], ["값을 이루는 비트", "0100 0001"], ["타입의 해석", "문자 A"]], "tone": "good" }
  ]
}
</script>
</x-compare>

값을 이루는 비트는 **완전히 같습니다.** `int` 는 앞에 0을 24개 더 채워 4바이트로 두고,
`char` 는 8개 더 채워 2바이트로 둘 뿐입니다. 다른 건 그 비트를 무엇으로 읽느냐입니다.

그래서 타입은 세 가지를 한꺼번에 정합니다.

> **타입 = 크기 + 해석 + 허용 연산**

`bool` 에 `- 1` 을 못 하는 이유도 여기 있습니다. 크기와 해석이 정해지면
그 위에서 할 수 있는 연산도 따라서 정해집니다.

## 기본 타입에는 무엇이 들어가나

타입은 크기만 정하는 게 아니라 **무엇을 담는지**도 정합니다.
종류별로 나눠서 보겠습니다.

### 정수 (소수점이 없는 수)

`3`, `-42`, `0` 처럼 딱 떨어지는 수입니다. 개수를 세거나 번호를 매길 때 씁니다.

| 타입 | 크기 | 담을 수 있는 값 | 예시 |
| --- | --- | --- | --- |
| `byte` | 1바이트 | 0 ~ 255 | `byte hp = 100;` |
| `short` | 2바이트 | -32,768 ~ 32,767 | `short level = 30;` |
| `int` | 4바이트 | 약 -21억 ~ 21억 | `int score = 12000;` |
| `long` | 8바이트 | 약 -922경 ~ 922경 | `long userId = 8_400_000_000L;` |

**기본은 `int` 입니다.** 그냥 `10` 이라고 쓰면 `int` 로 봅니다.
`long` 값을 쓸 때 뒤에 `L` 을 붙이는 이유가 이것입니다.

### 실수 (소수점이 있는 수)

`0.5`, `3.14`, `-0.001` 처럼 **소수점이 붙는 수**를 실수라고 부릅니다.
수학에서 쓰는 그 말이고, 프로그래밍에서는 "소수점이 있는 수" 정도로 보면 됩니다.

거리 · 속도 · 확률 · 시간처럼 **딱 떨어지지 않는 값**에 씁니다.

| 타입 | 크기 | 담을 수 있는 값 | 예시 |
| --- | --- | --- | --- |
| `float` | 4바이트 | 믿을 수 있는 자릿수 약 7자리 | `float speed = 5.5f;` |
| `double` | 8바이트 | 믿을 수 있는 자릿수 약 15~16자리 | `double pi = 3.14159;` |
| `decimal` | 16바이트 | 약 28자리, 돈 계산용 | `decimal price = 1200.50m;` |

여기서 "범위" 대신 **"믿을 수 있는 자릿수"** 라고 쓴 게 눈에 걸릴 겁니다.
정수와 달리 실수는 **범위 안에 있어도 값이 정확하지 않을 수 있습니다.**
이것도 크기가 정해져 있어서입니다. `0.1` 을 2진수로 바꾸면 소수점 아래가 끝없이 이어지는데,
`double` 은 8바이트 안에 담아야 하니 중간에서 잘라 가장 가까운 값을 저장합니다.

```csharp
Console.WriteLine(0.1 + 0.2);   // 0.30000000000000004
```

`0.1` 도 `0.2` 도 아주 조금씩 어긋난 채 저장돼 있어서, 더하면 그 어긋남이 드러납니다.
그래서 실수는 `==` 로 정확히 같은지 비교하지 않고, 돈처럼 소수점까지 정확해야 하는 값은 10진수로 저장하는 `decimal` 을 씁니다.

**기본은 `double` 입니다.** 그냥 `3.14` 라고 쓰면 `double` 로 봅니다.
그래서 `float` 에 넣을 때는 `5.5f` 처럼 `f` 를, `decimal` 은 `m` 을 붙여야 합니다.

### 참과 거짓

| 타입 | 크기 | 담을 수 있는 값 | 예시 |
| --- | --- | --- | --- |
| `bool` | 1바이트 | `true` 또는 `false` | `bool isAlive = true;` |

값이 둘뿐이라 1비트면 충분한데도 1바이트를 씁니다.
메모리를 비트 단위로 잘라 쓰는 것보다 바이트 단위로 다루는 게 훨씬 빠르기 때문입니다.

### 문자와 글

| 타입 | 크기 | 담을 수 있는 값 | 예시 |
| --- | --- | --- | --- |
| `char` | 2바이트 | 문자 **하나** | `char grade = 'A';` |
| `string` | 정해져 있지 않음 | 글자들 | `string name = "손오공";` |

여기 함정이 하나 있습니다. **따옴표가 다릅니다.**

```csharp
char c = 'A';      // 작은따옴표. 문자 하나
string s = "A";    // 큰따옴표. 글자 하나짜리 글

char x = "A";      // 컴파일 에러
```

`'A'` 와 `"A"` 는 눈에 거의 같아 보이지만 **다른 타입**입니다.
`char` 는 한 칸이고 `string` 은 글자들의 묶음입니다.

> `string` 만 크기가 "정해져 있지 않음" 인 게 눈에 띌 겁니다.
> 글자 수에 따라 달라지기 때문이고, 그래서 변수 자리에는 값이 아니라
> **"그 글이 어디에 있는지"**(주소, 64비트 환경에서 8바이트)가 들어갑니다.
> 글자들은 메모리의 다른 곳에 따로 놓입니다. 지금까지 본 타입들과 구조가 다릅니다.

### 외울 필요는 없습니다

코드로 물어보면 됩니다.

```csharp
Console.WriteLine(sizeof(int));      // 4
Console.WriteLine(int.MaxValue);     // 2147483647
Console.WriteLine(byte.MaxValue);    // 255
```

## 그러면 무조건 큰 걸 쓰면 되지 않나

값 하나만 보면 안 될 것도 없습니다. 개수가 많아질 때 이야기가 달라집니다.

<x-calc>
<script type="application/json">
{
  "label": "타입 하나 바꾸면 메모리가 얼마나 달라지나",
  "inputs": [
    { "id": "n", "label": "몬스터 수", "options": [["1천", 1000], ["10만", 100000], ["100만", 1000000], ["1000만", 10000000]], "default": 1000000 },
    { "id": "b", "label": "체력을 담는 타입", "options": [["byte", 1], ["short", 2], ["int", 4], ["long", 8]], "default": 4 }
  ],
  "bars": [
    { "label": "전부 long 으로", "expr": "8 * n / 1048576", "unit": "MB" },
    { "label": "고른 타입으로", "expr": "b * n / 1048576", "unit": "MB", "accent": true }
  ],
  "verdict": { "expr": "bars[1] < bars[0]", "yes": "아끼는 중, 필드 하나당 이만큼입니다", "no": "long 과 같거나 더 씁니다" },
  "note": "체력 필드 하나 기준입니다. 실제 객체에는 필드가 여러 개 있으니 차이는 이보다 커집니다."
}
</script>
</x-calc>

체력이 0~100 이면 `byte` 한 바이트로 충분한데 `long` 을 쓰면 8배를 씁니다.
그렇다고 처음부터 다 줄이라는 뜻은 아닙니다. 기준은 이렇습니다.

> **기본은 `int` 와 `double`. 줄이는 것은 이유가 생겼을 때만.**

`int` 가 기본인 이유는 요즘 CPU 가 32비트 단위 연산에 맞춰져 있어서
`short` 나 `byte` 로 계산해도 **더 빨라지지 않기 때문**입니다.
줄여서 이득을 보는 건 위 계산기처럼 **개수가 아주 많을 때, 메모리 쪽**입니다.

## 정리

- 변수는 메모리 한 자리에 붙인 이름이다. 이름은 컴파일이 끝나면 사라진다.
- 선언은 컴파일 시점에 자리와 크기를 정하는 일. 아무것도 쓰지 않는다.
- 할당은 실행 시점에 그 자리에 비트를 쓰는 일. `=` 는 "써라" 는 명령이다.
- 쓰지 않은 자리에는 이전 값이 남아 있다. 그래서 C# 이 읽기를 막는다.
- 타입은 크기와 해석과 허용 연산을 한꺼번에 정한다.
- 범위는 규칙이 아니라 비트 개수에서 나오는 계산 결과다.