---
title: "const 는 컴파일할 때, readonly 는 실행할 때 정해진다"
description: "둘 다 못 바꾸게 막는데 막히는 시점이 다릅니다. 그 차이가 남의 프로젝트에서 사고를 냅니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/advanced"
tags: ["C#"]
canonical: https://blog.unialgames.com/posts/csharp-readonly/
---
앞에서 `const` 를 봤습니다. 숫자를 코드에 그냥 박아 놓지 말고 이름을 붙이자는 이야기였습니다.

```csharp
const int 최대체력 = 100;
```

그때 안 물어본 것이 있습니다. **이 값은 언제 정해지나요.**

값을 못 바꾸게 막는 방법이 하나 더 있는데, 그 답이 서로 다릅니다.

> **`const` 는 컴파일할 때 값이 코드에 박힙니다. `readonly` 는 실행하다가 생성자에서 정해집니다.**

## readonly 는 이렇게 생겼다

필드 앞에 붙입니다. 여기까지는 `const` 와 같아 보입니다.

```csharp
class 캐릭터
{
    public readonly string 이름;
    public const int 최대레벨 = 99;

    public 캐릭터(string 이름)
    {
        this.이름 = 이름;   // 생성자에서는 넣을 수 있습니다
    }
}
```

두 가지가 눈에 띕니다.

`readonly` 는 선언할 때 값을 안 줘도 됩니다. 생성자에서 채우면 됩니다.
`const` 는 선언하는 자리에서 값을 줘야 합니다. 생성자에서 못 채웁니다.

이건 문법 취향이 아니라 **정해지는 시점이 달라서** 어쩔 수 없는 것입니다.
생성자는 실행할 때 도는 코드인데, `const` 는 실행하기 전에 이미 값이 있어야 하니까요.

## const 는 실행할 때 존재하지 않는다

`const` 는 변수가 아닙니다. 컴파일러가 **그 자리에 값을 그대로 써 넣고 사라집니다.**

<x-compare>
<script type="application/json">
{
  "tabs": [
    {
      "label": "내가 쓴 코드",
      "code": "const int 최대레벨 = 99;\n\nif (레벨 >= 최대레벨)\n{\n    Console.WriteLine(\"만렙\");\n}",
      "stats": [["실행 중 최대레벨 이라는 자리", "있어 보임"]]
    },
    {
      "label": "컴파일된 결과",
      "code": "// 최대레벨 은 사라졌습니다\n\nif (레벨 >= 99)\n{\n    Console.WriteLine(\"만렙\");\n}",
      "stats": [["실행 중 최대레벨 이라는 자리", "없음"]],
      "tone": "good"
    }
  ]
}
</script>
</x-compare>

읽어 오는 일 자체가 없으니 빠릅니다. 대신 **값을 바꿀 방법도 없습니다.**
실행 중에 바꾸고 말고가 아니라 바꿀 대상이 아예 없기 때문입니다.

`readonly` 는 반대입니다. 진짜 필드가 하나 있고, 거기 값이 들어 있고, 생성자가 끝난 뒤부터 못 씁니다.

## 그래서 담을 수 있는 것이 다르다

컴파일할 때 값이 정해져야 한다는 조건이 `const` 를 크게 제한합니다.

```csharp
const int a = 10;                        // 됩니다
const string b = "안녕";                  // 됩니다
const double c = 3.14 * 2;               // 됩니다. 계산도 컴파일러가 미리 합니다

const DateTime d = DateTime.Now;         // 에러
const int[] e = new int[3];              // 에러
const 점 f = new 점(1, 2);                // 에러
```

`DateTime.Now` 는 실행해 봐야 압니다. `new` 는 실행하면서 자리를 잡습니다.
컴파일러가 미리 알 수 없으니 `const` 가 될 수 없습니다.

`const` 로 쓸 수 있는 것은 숫자, `bool`, `char`, `string`, `enum` 뿐입니다.
나머지는 전부 `readonly` 로 가야 합니다.

```csharp
public static readonly DateTime 시작시각 = DateTime.Now;   // 됩니다
public static readonly int[] 기본값 = { 1, 2, 3 };         // 됩니다
```

## 남의 프로젝트에서 사고가 나는 자리

이게 `const` 의 진짜 함정입니다.

라이브러리 하나를 만들어서 `const int 버전 = 1;` 을 넣고 배포했다고 해 봅시다.
다른 프로젝트가 그 라이브러리를 참조해서 `Console.WriteLine(라이브러리.버전)` 을 씁니다.

그쪽 코드가 컴파일될 때 `1` 이 그대로 박힙니다.

이제 라이브러리를 고쳐 `const int 버전 = 2;` 로 바꾸고 새 DLL 만 갈아 끼웁니다.
쓰는 쪽은 다시 컴파일하지 않았습니다.

**여전히 1 이 찍힙니다.** DLL 은 2 인데 쓰는 쪽 코드에는 1 이 박혀 있으니까요.

`readonly` 였다면 실행할 때 DLL 에서 읽어 오므로 2 가 나왔을 겁니다.

그래서 규칙이 하나 나옵니다. **다른 프로젝트에서 참조할 수 있는 공개 값은 `const` 로 두지 않습니다.**
원주율처럼 영원히 안 바뀔 것만 `const` 이고, 나머지는 `static readonly` 입니다.

## readonly 는 상자를 잠그지 안을 잠그지 않는다

두 번째 함정입니다. 이건 값 타입과 참조 타입을 알면 저절로 설명됩니다.

```csharp
public readonly List<string> 기록 = new List<string>();

기록.Add("첫 줄");          // 됩니다
기록 = new List<string>();  // 에러
```

붙이는 것은 되고 갈아 끼우는 것은 안 됩니다. 뒤집힌 것처럼 보이지만 규칙은 하나입니다.

`readonly` 가 잠그는 것은 **그 변수 자리**입니다. 참조 타입 변수 안에 든 것은 주소였습니다.
그러니 잠긴 것은 주소입니다. 그 주소가 가리키는 리스트 안은 아무도 안 잠갔습니다.

값 타입이면 안까지 같이 잠깁니다. 자리 안에 값이 통째로 들어 있으니 잠글 것이 그것뿐이기 때문입니다.

내용까지 못 바꾸게 하려면 타입 자체를 바꿔야 합니다.

```csharp
public readonly IReadOnlyList<string> 기록 = new List<string>();
```

## 언제 무엇을 쓰나

| | `const` | `readonly` |
| --- | --- | --- |
| 정해지는 시점 | 컴파일할 때 | 생성자가 끝날 때 |
| 실행 중 존재 | 안 함 (값이 박힘) | 함 (진짜 필드) |
| 담을 수 있는 것 | 숫자, bool, char, string, enum | 전부 |
| 생성자에서 넣기 | 안 됨 | 됨 |
| 객체마다 다르게 | 안 됨 | 됨 |
| DLL 만 갈아 끼우면 | 안 바뀜 | 바뀜 |

고르는 순서는 이렇게 잡으면 됩니다.

1. 정말 영원히 안 바뀌는 값이고 밖에 공개 안 할 것이면 `const`
2. 객체마다 다르게 정해져야 하면 `readonly`
3. 나머지는 `static readonly`

## 정리

- `const` 는 컴파일할 때 **값이 코드에 박히고 사라집니다.** 실행 중에는 그런 자리가 없습니다
- `readonly` 는 진짜 필드입니다. 생성자에서 채우고 그다음부터 못 바꿉니다
- `const` 는 컴파일러가 미리 알 수 있는 것만 담습니다. `new` 나 `DateTime.Now` 는 안 됩니다
- 공개하는 값을 `const` 로 두면 DLL 을 갈아 끼워도 쓰는 쪽은 옛날 값을 씁니다
- `readonly` 는 자리를 잠그지 안을 잠그지 않습니다. `readonly List` 에 `Add` 는 됩니다

`const` 는 컴파일하면 값이 코드에 박히고 **사라집니다.** 컴파일이 지우는 것이 있다는 뜻입니다.