---
title: "같은지 보는 것과 줄 세우는 것은 다른 일이다"
description: "== 는 문자열의 내용을 비교합니다. 대소문자를 무시하려면 ToLower 로 맞추는 대신 비교 방식을 넘겨야 하고, 정렬 순서가 필요하면 Compare 를 씁니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-string-compare/
---
앞 글에서 `Replace` 가 대소문자를 구분한다고 했습니다. 그러면 이건 어떨까요.

```csharp
Console.WriteLine("Hello" == "hello");
```

답은 `False` 입니다. 그런데 여기서 물어볼 게 하나 더 있습니다.
**애초에 `==` 로 문자열을 비교하는 게 되는 게 맞나요.**

## `==` 는 내용을 비교한다

됩니다. 그리고 우리가 기대하는 대로 동작합니다.

```csharp
string a = "Hello";
string b = "Hel" + "lo";

Console.WriteLine(a == b);   // True
```

`a` 와 `b` 는 각각 다른 자리에 만들어진 문자열인데도 `True` 입니다.
**두 문자열이 담고 있는 글자를 처음부터 끝까지 비교**하기 때문입니다.

당연해 보이지만 당연한 일은 아닙니다. C# 에서 `==` 는 보통 **담긴 값이 같은지**를 봅니다.
문자열처럼 크기가 정해지지 않은 것은 변수에 내용 자체가 아니라 **내용이 있는 자리**가
담기는데, 그러면 `==` 는 자리가 같은지를 보게 됩니다. 자리가 다르면 내용이 같아도 `False` 입니다.

C# 은 문자열에 한해 `==` 를 **내용 비교로 따로 정해 두었습니다.** 문자열을 비교할 일이
너무 흔해서, 매번 메서드를 부르게 하는 대신 기호를 특별 대우한 것입니다.

> 변수에 값이 담기는 경우와 자리가 담기는 경우의 차이는 클래스를 다룰 때 제대로 봅니다.
> 지금은 **문자열의 `==` 는 내용을 본다**만 기억하면 됩니다.
> 다른 언어를 하다 온 경우 이 부분이 다를 수 있어서, 문자열 비교에 `==` 를 못 쓰는 언어도 있습니다.

`Equals` 도 결과가 같습니다.

```csharp
a.Equals(b);        // True
a == b;             // True
```

한쪽이 `null` 일 수 있으면 `==` 가 안전합니다. `a` 가 `null` 인데 `a.Equals(b)` 를 부르면
없는 것에게 말을 거는 것이라 예외가 납니다.

## 대소문자를 무시하려면

여기서 `ToLower()` 로 양쪽을 맞춰 비교하고 싶어집니다. 흔한 방법이고 대체로 동작합니다.
그런데 권하지 않습니다.

<x-compare>
<script type="application/json">
{
  "label": "대소문자 무시하고 비교하기",
  "tabs": [
    { "label": "ToLower 로 맞추기", "code": "if (a.ToLower() == b.ToLower())\n{\n    ...\n}", "stats": [["만들어지는 문자열", "2개"], ["언어 설정", "영향을 받음"], ["의도", "코드에 안 적혀 있음"]], "tone": "bad" },
    { "label": "비교 방식을 넘기기", "code": "if (a.Equals(b, StringComparison.OrdinalIgnoreCase))\n{\n    ...\n}", "stats": [["만들어지는 문자열", "0개"], ["언어 설정", "영향 없음"], ["의도", "이름에 적혀 있음"]], "tone": "good" }
  ]
}
</script>
</x-compare>

이유가 셋입니다.

**첫째, 문자열이 두 개 새로 만들어집니다.** 앞에서 본 그대로입니다. `ToLower()` 는
원본을 고치지 않고 새 문자열을 돌려주므로, 비교 한 번에 쓰고 버릴 문자열이 둘 생깁니다.
반복문 안이면 그만큼 쌓입니다.

**둘째, 결과가 컴퓨터의 언어 설정에 따라 달라질 수 있습니다.** 가장 유명한 예가 터키어입니다.
터키어에는 점 있는 `i` 와 점 없는 `ı` 가 서로 다른 글자로 있습니다.
그래서 터키어 설정에서는 `"I".ToLower()` 가 우리가 아는 `"i"` 가 아니라 점 없는 `"ı"` 입니다.

```csharp
// 터키어 설정의 컴퓨터에서
"IMAGE".ToLower() == "image"   // False
```

한국에서 만든 코드가 터키에서만 조용히 다르게 도는 셈입니다.
찾기 어려운 종류의 버그입니다.

**셋째, 의도가 코드에 안 적혀 있습니다.** `ToLower()` 두 번은 "대소문자를 무시하고 싶다" 가
아니라 "소문자로 바꾸고 싶다" 라고 적혀 있습니다. `OrdinalIgnoreCase` 는 이름이 곧 의도입니다.

## 서수 비교와 문화권 비교

`StringComparison` 에는 크게 두 갈래가 있습니다. 이름이 길어 보이지만 나누는 기준은 하나입니다.
**글자에 붙은 번호를 볼 것인가, 사람이 쓰는 언어의 규칙을 볼 것인가.**

| 값 | 무엇을 보나 |
| --- | --- |
| `Ordinal` | 앞에서 본 글자 번호를 그대로 비교 |
| `OrdinalIgnoreCase` | 번호 비교, 단 대소문자는 같게 취급 |
| `CurrentCulture` | 실행하는 컴퓨터의 언어 규칙 |
| `CurrentCultureIgnoreCase` | 언어 규칙, 대소문자 무시 |
| `InvariantCulture` | 어디서 돌려도 같은 언어 규칙 |

차이가 드러나는 자리는 **정렬**입니다.

```text
"Zebra" 와 "apple" 중 어느 쪽이 앞인가

Ordinal        →  "Zebra"   'Z' 는 90, 'a' 는 97
CurrentCulture →  "apple"   사전에서는 a 가 z 앞에 온다
```

앞에서 대문자 번호가 소문자 번호보다 작다고 했습니다. 그래서 번호로만 세우면
**대문자로 시작하는 것이 전부 앞으로 몰립니다.** 사람이 기대하는 사전 순서가 아닙니다.

고르는 기준은 단순합니다.

> **사람에게 보여줄 것이면 문화권 비교, 프로그램이 쓸 것이면 서수 비교.**

화면에 뿌릴 이름 목록을 정렬한다면 문화권 비교가 맞습니다.
반대로 아이디, 파일 경로, 설정 키, 통신에 쓰는 값처럼 **사람이 읽으라고 만든 게 아닌 문자열**은
서수 비교가 맞습니다. 빠르고, 어느 나라에서 돌려도 결과가 같습니다.

비교 방식을 안 넘기면 메서드마다 기본값이 다릅니다. `==` 와 `Equals` 는 서수 비교이고,
`string.Compare` 는 문화권 비교입니다. 헷갈리기 쉬운 자리라, 대소문자나 정렬이 걸린
비교라면 방식을 직접 적어 주는 편이 낫습니다.

## 순서를 알려면 `Compare`

`==` 는 같은지 아닌지만 알려줍니다. 정렬하려면 **어느 쪽이 앞인지**를 알아야 합니다.

```csharp
string.Compare("apple", "banana");   // 음수   apple 이 앞
string.Compare("banana", "apple");   // 양수   banana 가 뒤
string.Compare("apple", "apple");    // 0      같다

"apple".CompareTo("banana");         // 음수   같은 일을 하는 메서드
```

**중요한 것은 부호뿐입니다.** 음수면 앞, 0이면 같음, 양수면 뒤입니다.
`-1` 이 나오는 경우가 많지만 그 숫자에 의미가 있는 건 아니라서,
`Compare(a, b) == -1` 처럼 값을 직접 비교하면 안 됩니다.

```csharp
if (string.Compare(a, b) < 0)   // 이렇게
if (string.Compare(a, b) == -1) // 이렇게 말고
```

서수 비교라면 이 부호가 어디서 나오는지도 뻔합니다. **처음으로 다른 글자를 만난 자리에서
두 글자의 번호를 뺀 것**입니다. 앞에서 본 번호가 그대로 쓰입니다.

## 비어 있는 문자열

비교에서 자주 걸리는 마지막 자리입니다. `null` 과 `""` 는 다릅니다.

```csharp
string a = null;   // 문자열이 아예 없다
string b = "";     // 문자열은 있는데 글자가 0개다
```

`a.Length` 는 예외가 나고 `b.Length` 는 `0` 입니다. 둘을 한 번에 확인하는 메서드가 있습니다.

```csharp
string.IsNullOrEmpty(input);        // null 이거나 "" 이면 true
string.IsNullOrWhiteSpace(input);   // 위에 더해 공백뿐이어도 true
```

사용자 입력에는 두 번째가 맞습니다. 스페이스만 치고 엔터를 누른 입력은
`""` 가 아니라 `" "` 라서 첫 번째로는 안 걸립니다.
앞 글에서 입력을 받자마자 `Trim` 하라고 한 것과 같은 이야기입니다.

## 정리

- 문자열의 `==` 는 내용을 비교한다. C# 이 문자열에만 특별히 그렇게 정해 두었다.
- `Equals` 도 같지만, 한쪽이 `null` 일 수 있으면 `==` 가 안전하다.
- 대소문자를 무시할 때 `ToLower()` 로 맞추지 않는다. 문자열이 두 개 생기고, 언어 설정을 탄다.
- `Equals(b, StringComparison.OrdinalIgnoreCase)` 처럼 비교 방식을 넘긴다.
- 서수 비교는 글자 번호를, 문화권 비교는 언어의 정렬 규칙을 본다.
- 사람에게 보여줄 것이면 문화권 비교, 프로그램이 쓸 것이면 서수 비교.
- `Compare` 와 `CompareTo` 는 부호만 의미가 있다. `== -1` 로 비교하지 않는다.
- `null` 과 `""` 는 다르다. 사용자 입력은 `IsNullOrWhiteSpace` 로 확인한다.