---
title: "ref 는 고치러, out 은 채우러 간다"
description: "하나는 이미 있는 값을 바꾸러 가고, 하나는 없던 값을 만들어 옵니다. TryParse 가 두 번째 방식입니다."
author: "유니얼"
pubDate: 2026-09-08
category: "programming/csharp/basics"
tags: ["C#", "기초"]
canonical: https://blog.unialgames.com/posts/csharp-ref-out/
---
앞 글에서 매개변수는 사본이라고 했습니다. 대부분은 그게 맞습니다.
그런데 두 가지 경우에 곤란합니다.

**첫째, 값 두 개를 돌려줘야 할 때.** `return` 은 하나만 돌려줍니다.
나눗셈의 몫과 나머지를 같이 돌려주고 싶으면 방법이 없습니다.

**둘째, 실패할 수 있을 때.** 문자열을 숫자로 바꾸는 메서드는
"바꾼 값" 과 "바꿀 수 있었는지" 둘 다 알려줘야 합니다.

앞에서 이런 코드를 여러 번 봤습니다.

```csharp
if (int.TryParse(input, out int number))
{
    Console.WriteLine(number);
}
```

`out` 이 그때마다 나왔는데 설명을 미뤄뒀습니다. 여기서 정리합니다.

> **사본 말고 자리 자체를 넘기는 방법이 둘 있습니다. `ref` 와 `out` 입니다.**

## ref 는 원본 자리를 넘긴다

```csharp
void AddTen(ref int n)
{
    n = n + 10;
}

int score = 50;
AddTen(ref score);
Console.WriteLine(score);    // 60
```

앞 글의 코드에 `ref` 두 개를 붙였을 뿐인데 결과가 달라졌습니다.

`ref` 를 붙이면 값을 복사하지 않습니다.
**메서드 안의 `n` 과 밖의 `score` 가 같은 자리를 가리킵니다.**
이름이 두 개일 뿐 자리는 하나입니다.

<x-flow>
<script type="application/json">
{
  "label": "자리가 하나다",
  "dwell": 1.5,
  "lines": [
    "void AddTen(ref int n)",
    "{",
    "    n = n + 10;",
    "}",
    "",
    "int score = 50;",
    "AddTen(ref score);",
    "Console.WriteLine(score);"
  ],
  "cases": [
    {
      "label": "ref 를 붙이면",
      "state": "score = 50",
      "path": [
        { "line": 5, "why": "자리 하나가 생기고 50 이 들어갑니다.", "state": "score = 50" },
        { "line": 6, "why": "값이 아니라 자리를 넘깁니다. 호출하는 쪽에도 ref 를 씁니다." },
        { "line": 0, "why": "n 은 새 자리가 아닙니다. score 의 다른 이름입니다.", "state": "score(=n) = 50" },
        { "line": 2, "why": "n 을 고치면 score 가 고쳐집니다. 같은 자리니까요.", "state": "score(=n) = 60" },
        { "line": 7, "why": "메서드가 끝나도 값은 그대로 남아 있습니다.", "state": "score = 60", "out": "60" }
      ],
      "skip": [1, 3],
      "note": "앞 글의 같은 코드에서는 두 자리였고 밖은 50 이었습니다."
    }
  ]
}
</script>
</x-flow>

쓸 때 지켜야 할 것이 두 가지입니다.

**정의와 호출 양쪽에 `ref` 를 씁니다.** 호출하는 쪽에도 적게 한 것은 일부러입니다.
`AddTen(score)` 라고만 적혀 있으면 읽는 사람은 밖의 값이 바뀔 거라고 생각하지 못합니다.
`ref` 가 눈에 보여야 "이건 바뀔 수 있다" 를 압니다.

**넘기기 전에 값이 들어 있어야 합니다.**

```csharp
int score;
AddTen(ref score);      // 컴파일 에러: score 에 아직 값이 없습니다
```

`ref` 는 **이미 있는 값을 고치러** 가는 것이라, 고칠 값이 없으면 말이 안 됩니다.

## out 은 빈 통을 주고 채워 받는다

`out` 은 반대입니다. **값을 안 넣고 넘깁니다.** 대신 메서드가 채워 줍니다.

```csharp
void Divide(int a, int b, out int quotient, out int remainder)
{
    quotient = a / b;
    remainder = a % b;
}

Divide(17, 5, out int q, out int r);
Console.WriteLine(q + " ... " + r);    // 3 ... 2
```

`return` 으로는 하나만 돌려주는데, `out` 을 두 개 붙여서 두 개를 받았습니다.

여기도 규칙이 둘입니다.

**넘기기 전에 값이 없어도 됩니다.** 오히려 그게 보통입니다. 받으러 가는 것이니까요.

**메서드 안에서 반드시 값을 넣어야 합니다.**

```csharp
void Divide(int a, int b, out int quotient, out int remainder)
{
    quotient = a / b;
    // remainder 를 안 채우면 컴파일 에러
}
```

빈 통을 받았으면 채워서 보내야 합니다. 안 채우고 끝내는 길이 하나라도 있으면 컴파일이 안 됩니다.
그래서 **부른 쪽은 값이 들어 있다고 믿어도 됩니다.**

## 둘의 차이

| | `ref` | `out` |
| --- | --- | --- |
| 넘기기 전 | 값이 들어 있어야 한다 | 없어도 된다 |
| 메서드 안에서 | 안 바꿔도 된다 | 반드시 값을 넣어야 한다 |
| 방향 | 들고 갔다가 고쳐서 온다 | 빈 통을 주고 채워 온다 |
| 쓰는 자리 | 이미 있는 값을 바꾸는 것이 목적 | 결과를 여러 개 받아야 할 때 |

한 문장으로 하면 이렇습니다.

> **`ref` 는 고치러 가고, `out` 은 채우러 갑니다.**

## TryParse 가 바로 이 방식이다

이제 앞에서 계속 보이던 그 모양이 읽힙니다.

```csharp
if (int.TryParse(input, out int number))
{
    Console.WriteLine(number);
}
```

`TryParse` 는 두 가지를 알려줘야 합니다.
**바꿀 수 있었는가**와 **바꾼 값**입니다.

- 성공 여부는 `bool` 로 `return` 합니다. 그래야 `if` 괄호에 바로 넣을 수 있습니다.
- 바꾼 값은 `out` 으로 채워 줍니다.

실패하면 `out` 에는 `0` 이 들어갑니다. 채우기는 채워야 하니까요.
그래서 **`return` 값을 확인하지 않고 `number` 만 쓰면 안 됩니다.**
실패한 `0` 인지 진짜 `0` 인지 구분이 안 됩니다.

`Dictionary` 의 `TryGetValue`, `Stack` 의 `TryPop` 도 같은 모양입니다.
**실패할 수 있는 일은 성공 여부를 돌려주고 결과는 `out` 으로 채운다.**
이 방식이 C# 전반에 반복됩니다. 앞으로 `Try` 로 시작하는 이름을 보면 이 모양이라고 보면 됩니다.

`out` 변수는 넘기는 자리에서 바로 선언할 수 있습니다.

```csharp
int number;
if (int.TryParse(input, out number))     // 미리 선언해도 되고

if (int.TryParse(input, out int number)) // 그 자리에서 선언해도 됩니다
```

아래쪽이 짧고, 그 변수가 어디서 왔는지도 한 줄에 보입니다.

## 자주 쓰지는 않습니다

`ref` 와 `out` 은 알아 둬야 하지만 **자주 쓰는 도구는 아닙니다.**

값을 하나 돌려주면 되는 일은 그냥 `return` 이 낫습니다.
읽는 사람이 "이 메서드는 무엇을 돌려주는가" 를 반환 타입 한 곳에서 봅니다.
`out` 이 늘어나면 결과가 여기저기 흩어져서 오히려 읽기 어려워집니다.

`out` 이 정말 어울리는 자리는 **`Try` 패턴** 처럼
"성공 여부와 값을 같이 줘야 하는" 경우입니다.
그 외에는 값을 묶어서 하나로 돌려주는 방법이 보통 더 낫습니다.

## 정리

- 매개변수는 기본이 사본입니다. `ref` 와 `out` 은 자리 자체를 넘깁니다.
- `ref` 는 밖의 변수와 같은 자리를 가리킵니다. 안에서 고치면 밖이 고쳐집니다.
- `ref` 는 정의와 호출 양쪽에 적어야 하고, 넘기기 전에 값이 들어 있어야 합니다.
- `out` 은 값 없이 넘기고 메서드가 채웁니다. 안 채우면 컴파일 에러입니다.
- `ref` 는 고치러 가고 `out` 은 채우러 갑니다.
- `Try` 로 시작하는 메서드가 이 방식입니다. 성공 여부는 `return`, 값은 `out`.
- 둘 다 자주 쓰지는 않습니다. 하나면 `return` 이 낫습니다.