---
title: "값이 없다는 것도 상태다"
description: "null 은 0이 아니라 가리키는 대상이 없는 것입니다. 없을 수 있다는 사실을 타입에 적어 두면 컴파일러가 봐 줍니다."
author: "유니얼"
pubDate: 2026-09-09
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-null/
---
`NullReferenceException` 은 C# 에서 가장 자주 만나는 예외입니다.
앞에서 원인은 언제나 같다고 했습니다. **아무도 안 가리키는 것을 따라간 것**입니다.

그런데 왜 이렇게 자주 날까요.

```csharp
string name = FindName(id);
Console.WriteLine(name.Length);
```

`FindName` 이 못 찾으면 무엇을 돌려줄까요. 아마 `null` 일 겁니다.
그런데 **그 사실이 코드 어디에도 안 적혀 있습니다.** 타입은 그냥 `string` 입니다.

`string` 이라고 적혀 있으면 문자열이 온다고 읽습니다.
`null` 이 올 수도 있다는 것은 `FindName` 안을 열어 봐야 압니다.
그래서 확인을 빠뜨리고, 실행하다가 터집니다.

> **없을 수 있다는 것을 타입에 적어 두면 컴파일러가 확인해 줍니다.**

## 값 타입에는 원래 null 이 없었다

먼저 값 타입부터 봅니다.

```csharp
int n = null;      // 컴파일 에러
```

`int` 는 4바이트 자리에 숫자가 들어 있습니다. **비어 있는 상태가 없습니다.**
어떤 비트 조합이든 어떤 숫자로 읽힙니다. `0` 도 값이지 "없음" 이 아닙니다.

그런데 없음이 필요할 때가 있습니다.

- 아직 입력하지 않은 나이
- 아직 안 정한 점수
- 데이터베이스에서 비어 있는 칸

`0` 으로 쓰면 안 됩니다. 0살인 것과 아직 안 넣은 것을 구분할 수 없습니다.
앞에서 `Age` 를 안 넣어서 `0` 이 됐을 때 문제였던 그 상황입니다.

물음표를 붙이면 됩니다.

```csharp
int? age = null;

if (age.HasValue)
{
    Console.WriteLine(age.Value);
}
```

`int?` 는 **`int` 아니면 없음**을 담을 수 있는 타입입니다.
`HasValue` 로 확인하고 `Value` 로 꺼냅니다.

```csharp
int n = age ?? 0;      // 없으면 0으로
```

`??` 는 왼쪽이 `null` 이면 오른쪽을 쓰라는 뜻입니다. 곧 다시 나옵니다.

## 참조 타입에는 원래 null 이 있었다

참조 타입은 반대입니다. **처음부터 `null` 이 될 수 있었습니다.**
주소를 담는 자리인데 "아직 아무 데도 안 가리킴" 이 자연스러운 상태니까요.

문제는 그게 **구분이 안 된다**는 것입니다.

```csharp
string a = "안녕";      // 항상 값이 있는 것
string b = null;        // 없을 수도 있는 것
```

둘 다 타입이 `string` 입니다. 코드를 읽는 사람도, 컴파일러도 구분을 못 합니다.

그래서 요즘 C# 에는 **참조 타입에도 물음표를 붙이는** 문법이 있습니다.

```csharp
string name = "안녕";     // 없으면 안 되는 것
string? found = null;     // 없을 수도 있는 것
```

이걸 켜 두면 컴파일러가 봐 줍니다.

```csharp
string? found = FindName(id);
Console.WriteLine(found.Length);      // 경고: null 일 수 있습니다
```

```csharp
string name = null;                   // 경고: 물음표가 없는데 null 을 넣었습니다
```

**확인을 빠뜨린 자리를 컴파일할 때 알려 줍니다.** 실행해 볼 필요가 없습니다.
새 프로젝트를 만들면 대체로 켜져 있습니다.

물음표가 하는 일은 값을 바꾸는 것이 아닙니다.
**의도를 적어 두는 것**입니다. "여기는 없을 수 있다" 를 타입에 남기면
읽는 사람도 알고 컴파일러도 압니다.

## null 검사를 짧게 쓰는 기호 세 가지

`if (x != null)` 을 계속 적는 것이 번거로워서 짧게 쓰는 문법이 있습니다.

**`?.` 앞이 null 이면 그냥 넘어간다**

```csharp
int? len = name?.Length;
```

`name` 이 `null` 이면 `Length` 를 안 물어보고 결과가 `null` 이 됩니다.
안 터집니다. 결과가 `null` 일 수 있으니 받는 쪽이 `int?` 입니다.

이어 붙일 수도 있습니다.

```csharp
string? city = player?.Address?.City;
```

중간 어디가 `null` 이든 그 자리에서 멈추고 `null` 이 됩니다.
`if` 를 세 겹으로 쓰던 것이 한 줄이 됩니다.

앞에서 이벤트를 부를 때 쓴 `Died?.Invoke()` 가 이것입니다.

**`??` 앞이 null 이면 오른쪽을 쓴다**

```csharp
string display = name ?? "이름 없음";
int count = list?.Count ?? 0;
```

`?.` 와 짝으로 자주 씁니다. "없으면 이걸로" 라는 기본값을 적는 자리입니다.

**`??=` 비어 있을 때만 넣는다**

```csharp
list ??= new List<int>();
```

`list` 가 `null` 일 때만 새로 만듭니다. 이미 있으면 그대로 둡니다.

```csharp
if (list == null) list = new List<int>();
```

이 두 줄과 같습니다.

## 없음을 애초에 안 만드는 것이 낫다

문법이 편해졌다고 `null` 을 많이 쓰라는 뜻은 아닙니다.
**없을 수 있는 자리를 줄이는 것**이 먼저입니다.

목록을 돌려주는 메서드를 보겠습니다.

```csharp
List<int>? GetItems()
{
    if (없으면) return null;
    ...
}
```

```csharp
List<int> GetItems()
{
    if (없으면) return new List<int>();     // 빈 목록
    ...
}
```

아래쪽이 낫습니다. **빈 목록은 그냥 돌면 0번 도는 것**이라 부르는 쪽이 확인할 것이 없습니다.
`null` 을 돌려주면 부르는 모든 곳에서 확인해야 합니다.

문자열도 같습니다. `null` 대신 빈 문자열을 돌려주면 대부분 그냥 동작합니다.

앞에서 생성자로 상태를 지키는 이야기를 했습니다. 여기도 같은 자리입니다.
**만들 때 채워 두면 `null` 이 될 일이 없습니다.**

```csharp
class Player
{
    public string Name { get; }
    public List<Item> Items { get; } = new List<Item>();

    public Player(string name)
    {
        Name = name;
    }
}
```

`Items` 는 절대 `null` 이 아닙니다. 만들 때 빈 목록으로 채웠습니다.
쓰는 쪽에서 확인할 일이 없어집니다.

## 정리

- `null` 은 0이 아니라 가리키는 대상이 없는 상태입니다.
- 값 타입에는 원래 없음이 없습니다. `int?` 처럼 물음표를 붙이면 담을 수 있습니다.
- 참조 타입은 원래 `null` 이 될 수 있었는데, 그 사실이 타입에 안 적혀 있었습니다.
- `string?` 처럼 적어 두면 컴파일러가 확인 빠뜨린 자리를 알려 줍니다.
- 물음표는 값을 바꾸는 것이 아니라 **의도를 적어 두는 것**입니다.
- `?.` 는 앞이 `null` 이면 넘어가고, `??` 는 없으면 오른쪽을 쓰고, `??=` 는 비어 있을 때만 넣습니다.
- 문법보다 중요한 것은 없을 수 있는 자리를 줄이는 것입니다.
- 빈 목록이나 빈 문자열을 돌려주면 부르는 쪽이 확인할 것이 없어집니다.