---
title: "2차원 배열은 행과 열로 찾는다"
description: "격자로 보이지만 메모리에는 여전히 한 줄로 놓여 있습니다. 번호 두 개가 한 자리를 가리키는 방식입니다."
author: "유니얼"
pubDate: 2026-09-08
category: "programming/csharp/collections"
tags: ["C#", "자료구조"]
canonical: https://blog.unialgames.com/posts/csharp-array-2d/
---
앞 글의 배열은 칸이 한 줄이었습니다. 그런데 다루려는 것이 원래 격자인 경우가 있습니다.
바둑판, 지도, 표 같은 것들입니다.

한 줄짜리 배열로도 할 수는 있습니다. 3 × 3 판이면 9칸짜리 배열을 만들고
직접 계산해서 쓰면 됩니다.

```csharp
int[] board = new int[9];

board[1 * 3 + 2] = 1;      // 1행 2열
```

돌아가긴 합니다. 다만 `1 * 3 + 2` 를 매번 손으로 적어야 하고,
판 크기가 바뀌면 곱하는 숫자를 전부 고쳐야 합니다.

> **번호 두 개로 칸을 가리키게 해 주는 것이 2차원 배열입니다.**

## 2차원 배열은 이렇게 생겼다

```csharp
int[,] board = new int[3, 3];

board[1, 2] = 1;
Console.WriteLine(board[1, 2]);    // 1
```

대괄호 안에 쉼표가 하나 들어갔습니다. 이게 "번호를 두 개 받는다" 는 표시입니다.
`new int[3, 3]` 은 3행 3열, 그러니까 아홉 칸입니다.

값을 미리 적을 때는 중괄호를 겹쳐 씁니다.

```csharp
int[,] board =
{
    { 1, 0, 0 },
    { 0, 1, 0 },
    { 0, 0, 1 },
};
```

적은 모양이 그대로 판 모양입니다. 첫 번호가 행(줄), 둘째 번호가 열(칸)입니다.

## 격자로 보이지만 실제로는 한 줄이다

여기가 이 글에서 짚고 갈 부분입니다.

메모리는 격자가 아닙니다. 앞에서 본 대로 **주소가 0번부터 쭉 이어지는 한 줄**입니다.
2차원 배열도 거기에 놓입니다.

<figure class="diagram">
<svg viewBox="0 0 680 240" role="img" aria-label="3행 3열 격자가 메모리에서는 한 줄로 이어져 놓이는 것을 보여주는 그림">
<text x="20" y="24" font-size="13" fill="var(--text-muted)">코드에서 보는 모양</text>
<g fill="var(--bg)" stroke="var(--border)">
<rect x="24" y="38" width="40" height="40" rx="5"></rect>
<rect x="68" y="38" width="40" height="40" rx="5"></rect>
<rect x="112" y="38" width="40" height="40" rx="5"></rect>
<rect x="24" y="82" width="40" height="40" rx="5"></rect>
<rect x="68" y="82" width="40" height="40" rx="5"></rect>
<rect x="112" y="82" width="40" height="40" rx="5"></rect>
<rect x="24" y="126" width="40" height="40" rx="5"></rect>
<rect x="68" y="126" width="40" height="40" rx="5"></rect>
<rect x="112" y="126" width="40" height="40" rx="5"></rect>
</g>
<g text-anchor="middle" font-size="11" fill="var(--text-muted)" class="bit">
<text x="44" y="63">0,0</text><text x="88" y="63">0,1</text><text x="132" y="63">0,2</text>
<text x="44" y="107">1,0</text><text x="88" y="107">1,1</text><text x="132" y="107">1,2</text>
<text x="44" y="151">2,0</text><text x="88" y="151">2,1</text><text x="132" y="151">2,2</text>
</g>
<text x="200" y="24" font-size="13" fill="var(--text-muted)">메모리에 놓인 모양</text>
<g fill="var(--bg)" stroke="var(--border)">
<rect x="200" y="38" width="46" height="40" rx="5"></rect>
<rect x="250" y="38" width="46" height="40" rx="5"></rect>
<rect x="300" y="38" width="46" height="40" rx="5"></rect>
<rect x="354" y="38" width="46" height="40" rx="5"></rect>
<rect x="404" y="38" width="46" height="40" rx="5"></rect>
<rect x="454" y="38" width="46" height="40" rx="5"></rect>
<rect x="508" y="38" width="46" height="40" rx="5"></rect>
<rect x="558" y="38" width="46" height="40" rx="5"></rect>
<rect x="608" y="38" width="46" height="40" rx="5"></rect>
</g>
<g text-anchor="middle" font-size="11" fill="var(--text-muted)" class="bit">
<text x="223" y="63">0,0</text><text x="273" y="63">0,1</text><text x="323" y="63">0,2</text>
<text x="377" y="63">1,0</text><text x="427" y="63">1,1</text><text x="477" y="63">1,2</text>
<text x="531" y="63">2,0</text><text x="581" y="63">2,1</text><text x="631" y="63">2,2</text>
</g>
<g stroke="var(--border)" stroke-width="1.5" fill="none">
<path d="M200 88 L346 88"></path>
<path d="M354 88 L500 88"></path>
<path d="M508 88 L654 88"></path>
</g>
<g text-anchor="middle" font-size="12" fill="var(--accent)">
<text x="273" y="106">0행</text>
<text x="427" y="106">1행</text>
<text x="581" y="106">2행</text>
</g>
<text x="200" y="146" font-size="13" fill="var(--text-muted)">한 행을 다 놓고 나서 다음 행을 이어 놓는다</text>
<text x="200" y="180" font-size="14" fill="var(--text)" font-family="var(--font-mono)">board[1, 2]</text>
<text x="200" y="206" font-size="13" fill="var(--text-muted)">→ 앞에 놓인 칸 수 = 1 × 3 + 2 = 5</text>
<text x="200" y="228" font-size="13" fill="var(--text-muted)">→ 시작 주소에서 5칸 떨어진 자리</text>
</svg>
<figcaption>행 단위로 이어 붙인다. 번호 두 개는 결국 한 번호로 계산된다.</figcaption>
</figure>

한 행을 다 놓고 나서 다음 행을 이어 놓습니다.
그래서 `board[행, 열]` 은 결국 **`행 × 열개수 + 열`** 번째 칸입니다.
맨 앞에서 우리가 손으로 계산했던 `1 * 3 + 2` 를 컴파일러가 대신 해 주는 것입니다.

문법이 바뀐 것이지 메모리가 격자가 된 것은 아닙니다.

## 이중 반복문으로 전부 훑는다

앞에서 바깥이 행, 안쪽이 열이라고 했습니다. 여기가 그 자리입니다.

```csharp
for (int row = 0; row < 3; row++)
{
    for (int col = 0; col < 3; col++)
    {
        Console.Write(board[row, col] + " ");
    }
    Console.WriteLine();
}
```

별찍기에서 `Write` 로 이어 찍고 `WriteLine()` 으로 줄을 바꿨던 것과 같습니다.
안쪽이 한 줄을 다 찍고, 바깥이 줄을 넘깁니다.

행과 열의 개수는 `GetLength` 로 받습니다.

```csharp
board.GetLength(0)      // 행 개수
board.GetLength(1)      // 열 개수
```

`Length` 는 전체 칸 수(9)를 줍니다. 반복문 조건에 그냥 쓰면 안 됩니다.

## 순서를 바꾸면 느려질 수 있다

두 반복문의 순서를 바꿔도 결과는 같습니다. 아홉 칸을 전부 방문하는 것은 똑같으니까요.

```csharp
for (int col = 0; col < 3; col++)
{
    for (int row = 0; row < 3; row++)
    {
        Console.Write(board[row, col] + " ");   // 열 방향으로 훑습니다
    }
}
```

3 × 3 이면 차이가 없습니다. 그런데 판이 커지면 이야기가 달라집니다.

메모리에는 행 순서로 놓여 있습니다. 행 방향으로 훑으면 **바로 옆 칸**으로 가고,
열 방향으로 훑으면 매번 한 행 크기만큼 건너뜁니다.
CPU 는 값 하나를 읽을 때 그 주변을 한 덩어리로 미리 가져다 두는데,
멀리 뛰면 그 덩어리가 매번 헛것이 됩니다.

<x-walk2d>
<script type="application/json">
{
  "label": "같은 칸을 다 도는데 메모리에서는 다르게 움직인다",
  "rows": 3,
  "cols": 4,
  "mode": "both",
  "note": "위가 코드에서 보는 격자, 아래가 메모리에 놓인 한 줄입니다. 아홉이 아니라 열두 칸이고, 어느 쪽으로 훑든 열두 칸을 다 방문합니다. 다른 것은 메모리에서 움직인 거리뿐입니다."
}
</script>
</x-walk2d>

**방문한 칸 수는 같은데 건너뛴 횟수가 다릅니다.**
행 우선은 열한 번 전부 바로 옆 칸이고, 열 우선은 열한 번 전부 건너뜁니다.
격자에서는 똑같이 한 칸씩 옮겨 다니는데 메모리에서는 그렇지 않습니다.

지금 단계에서 외울 것은 아닙니다. 다만 **격자로 보여도 아래는 한 줄**이라는 사실이
성능에 드러나는 자리가 있다는 것 정도는 알아두면 됩니다.

## 칸마다 길이가 다른 격자

행마다 칸 수가 다른 경우가 있습니다. 이때는 모양이 다릅니다.

```csharp
int[][] jagged = new int[3][];

jagged[0] = new int[2];
jagged[1] = new int[5];
jagged[2] = new int[1];

jagged[1][3] = 7;      // 대괄호가 두 벌입니다
```

`int[,]` 는 대괄호 한 벌에 쉼표, `int[][]` 는 대괄호 두 벌입니다.
생김새가 비슷해서 헷갈리는데 안이 완전히 다릅니다.

`int[,]` 는 통짜 한 줄이고, `int[][]` 는 **배열을 담는 배열**입니다.
바깥 배열의 칸마다 다른 배열의 주소가 들어 있습니다.
그래서 행마다 길이를 다르게 만들 수 있고, 대신 칸들이 한 줄로 붙어 있지 않습니다.

대부분은 `int[,]` 로 충분합니다.

## 정리

- `int[,]` 는 번호 두 개로 칸을 가리키는 배열입니다. 첫 번호가 행, 둘째가 열입니다.
- 격자로 보이지만 메모리에는 한 행씩 이어 붙여 한 줄로 놓입니다.
- `board[행, 열]` 은 `행 × 열개수 + 열` 번째 칸입니다.
- 전부 훑을 때는 이중 반복문을 씁니다. 바깥이 행, 안쪽이 열입니다.
- 행 개수는 `GetLength(0)`, 열 개수는 `GetLength(1)` 입니다. `Length` 는 전체 칸 수입니다.
- 행 방향으로 훑는 편이 메모리에 놓인 순서와 맞습니다.
- 행마다 길이가 다르면 `int[][]` 입니다. 배열을 담는 배열이라 안이 다릅니다.