---
title: "Dictionary 는 번호 대신 이름으로 찾는다"
description: "이름을 숫자로 바꿔서 자리를 계산합니다. 그래서 몇 개가 들어 있든 찾는 시간이 거의 같습니다."
author: "유니얼"
pubDate: 2026-09-08
category: "programming/csharp/collections"
tags: ["C#", "자료구조"]
canonical: https://blog.unialgames.com/posts/csharp-dictionary/
---
배열도 `List` 도 꺼낼 때 번호를 씁니다. 그런데 우리가 찾고 싶은 것이
번호가 아닐 때가 있습니다.

아이디로 사용자를 찾거나, 아이템 이름으로 가격을 찾는 일입니다.
`List` 로 하면 이렇게 됩니다.

```csharp
List<string> names = new List<string> { "포션", "검", "방패" };
List<int> prices = new List<int> { 50, 300, 200 };

int found = -1;

for (int i = 0; i < names.Count; i++)
{
    if (names[i] == "포션")
    {
        found = prices[i];
        break;
    }
}
```

이름 목록과 가격 목록을 **같은 번호끼리 짝**이 되도록 나란히 들고 있습니다.
둘의 순서가 어긋나면 값이 통째로 뒤섞이는데, 그걸 막아 주는 장치는 없습니다.

그리고 찾을 때까지 **앞에서부터 하나씩 비교**합니다.
운이 좋으면 첫 칸, 나쁘면 마지막 칸입니다. 없으면 전부 다 봅니다.
아이템이 만 개면 만 번 비교합니다.

앞 글에서 배열은 몇 번째 칸이든 찾는 시간이 같다고 했습니다.
**번호를 알 때만 그렇습니다.** 번호를 모르고 내용으로 찾으면 훑는 수밖에 없습니다.

> **이름을 번호로 바꿔 주면, 이름으로도 곧장 찾아갈 수 있습니다.**

## Dictionary 는 이렇게 생겼다

```csharp
using System.Collections.Generic;

Dictionary<string, int> prices = new Dictionary<string, int>();

prices["포션"] = 50;
prices["검"] = 300;

Console.WriteLine(prices["포션"]);     // 50
```

꺾쇠 안이 두 칸입니다. **앞이 찾을 때 쓰는 것(키), 뒤가 담을 것(값)** 입니다.
`Dictionary<string, int>` 는 문자열로 찾아서 정수를 꺼내는 것입니다.

대괄호 안에 번호 대신 키가 들어갑니다. 생김새는 배열과 비슷한데 안이 전혀 다릅니다.

| | 배열 · List | Dictionary |
| --- | --- | --- |
| 대괄호 안 | 번호 (0부터) | 키 (아무 값) |
| 순서 | 있다 | 보장하지 않는다 |
| 없는 것을 꺼내면 | 범위 밖 예외 | 키 없음 예외 |
| 중복 | 같은 값이 여러 칸에 있어도 된다 | 같은 키는 하나뿐 |

## 이름을 숫자로 바꿔서 자리를 계산한다

어떻게 훑지 않고 곧장 갈까요.

앞에서 글자에 번호가 붙어 있다고 했습니다. 문자열은 그 번호들이 줄지어 선 것입니다.
그러니까 **문자열도 결국 숫자 덩어리**입니다.
그 숫자들을 섞어서 값 하나로 만들면 이름이 숫자가 됩니다. 이걸 해시라고 합니다.

<x-hash>
<script type="application/json">
{
  "label": "이름으로 자리를 계산한다",
  "buckets": 6,
  "mode": "chain",
  "keys": [
    { "name": "포션", "hash": 17 },
    { "name": "검", "hash": 12 },
    { "name": "방패", "hash": 7 }
  ],
  "lookup": [
    { "name": "검", "hash": 12 }
  ],
  "note": "해시 값은 암산할 수 있게 작은 수로 잡았습니다. 17 % 6 = 5, 12 % 6 = 0, 7 % 6 = 1. 넣을 때와 찾을 때가 같은 계산입니다. 마지막에 검을 찾는데, 0번 칸만 보고 끝납니다. 나머지 칸은 들여다보지 않습니다."
}
</script>
</x-hash>

**넣을 때와 찾을 때가 같은 계산입니다.** 넣을 때 5번 칸이었으면 찾을 때도 5번 칸입니다.
그래서 찾아다닐 필요가 없습니다.

정리하면 `Dictionary` 는 **배열 위에 이름을 번호로 바꾸는 계산을 얹은 것**입니다.
번호를 우리가 아는 게 아니라 계산해서 얻는다는 것만 다릅니다.

그래서 들어 있는 것이 열 개든 만 개든 찾는 시간이 거의 같습니다.
`List` 에서 훑던 것과 여기가 다릅니다.

| | 번호로 꺼내기 | 내용으로 찾기 |
| --- | --- | --- |
| 배열 · List | 바로 | 전부 훑는다 |
| Dictionary | (번호가 없다) | 바로 |

"거의" 라고 쓴 이유가 있습니다. 서로 다른 이름이 같은 칸으로 계산될 수 있습니다.
그때는 그 칸에서 몇 번 더 비교합니다. 드물게 일어나고, 잘 만든 해시는 이걸 최대한 줄입니다.

## 없는 키를 꺼내면 터진다

```csharp
Console.WriteLine(prices["없는아이템"]);    // KeyNotFoundException
```

배열에서 없는 칸을 건드렸을 때와 같습니다. 그 자리에서 멈춥니다.

미리 확인하거나, 꺼내면서 확인할 수 있습니다.

```csharp
if (prices.ContainsKey("포션"))
{
    Console.WriteLine(prices["포션"]);
}
```

```csharp
if (prices.TryGetValue("포션", out int price))
{
    Console.WriteLine(price);
}
```

아래쪽이 낫습니다. 위쪽은 **찾는 일을 두 번** 합니다.
있는지 보려고 한 번, 꺼내려고 한 번입니다.
`TryGetValue` 는 한 번에 찾아서 있으면 값까지 담아 줍니다.

돌려주는 것이 값이 아니라 **성공했는지 여부**라는 점을 봐 두세요.
`out` 이 무엇인지는 메서드에서 다룰 텐데, 지금은 "실패할 수 있는 일은
성공 여부를 돌려주고 결과는 따로 담아 준다" 는 모양만 기억하면 됩니다.

## 넣는 방법이 두 가지다

```csharp
prices["포션"] = 50;      // 없으면 넣고, 있으면 덮어쓴다
prices.Add("포션", 50);   // 없으면 넣고, 있으면 예외
```

같은 일 같은데 이미 있는 키를 만났을 때가 다릅니다.

덮어써도 되는 상황이면 대괄호, **중복이 있으면 안 되는 상황**이면 `Add` 를 씁니다.
`Add` 가 터지는 것은 불편한 게 아니라 알려주는 것입니다.
같은 아이디가 두 번 등록되는 것이 버그라면, 조용히 덮어쓰는 쪽이 훨씬 나쁩니다.

## 순서를 믿으면 안 된다

`Dictionary` 는 넣은 순서를 지켜 주지 않습니다.
자리를 해시로 정하니 넣은 순서와 상관없는 자리에 놓입니다.

```csharp
foreach (var pair in prices)
{
    Console.WriteLine(pair.Key + ": " + pair.Value);
}
```

`foreach` 는 아직 안 다뤘습니다. 번호를 신경 쓰지 않고 들어 있는 것을 하나씩
꺼내 도는 반복문이라고만 보면 됩니다.

돌려는 볼 수 있지만 **나오는 순서를 기대하면 안 됩니다.**
순서가 중요하면 `List` 를 같이 쓰거나 정렬해서 써야 합니다.

## 정리

- `Dictionary` 는 번호 대신 키로 찾습니다. 꺾쇠 안이 키 타입, 값 타입 순서입니다.
- 키를 해시로 숫자를 만들고 그 숫자로 칸을 정합니다. 결국 번호로 찾는 것은 배열과 같습니다.
- 그래서 몇 개가 들어 있든 찾는 시간이 거의 같습니다. `List` 에서 훑던 것과 다릅니다.
- 없는 키를 꺼내면 예외입니다. `TryGetValue` 는 한 번에 확인하고 꺼냅니다.
- 대괄호로 넣으면 덮어쓰고, `Add` 로 넣으면 중복일 때 터집니다.
- 넣은 순서는 보장되지 않습니다.