1. 소개
C#에서는 기본적으로 모든 메서드 파라미터가 값으로 전달돼. 즉, 메서드에는 원본이 아니라 복사본이 들어가는 거지.
근데 만약 메서드 안에서 외부 변수를 바꿔야 한다면? 아니면 메서드에서 여러 값을 한 번에 반환하고 싶다면? 아니면 엄청 큰 구조체를 메서드에 넘겨야 하는데, 그걸 다 복사하기 싫고 그냥 참조만 넘겨서 메모리 아끼고 싶다면?
이 모든 게 바로 파라미터 수정자에 관한 거야. 이걸로 호출하는 코드랑 메서드 사이에서 데이터가 어떻게 전달되는지 직접 컨트롤할 수 있어.
수정자 종류
| 수정자 | 데이터 전달 | 호출 전에 초기화 필요? | 내부에서 읽기 가능? | 내부에서 쓰기 가능? | 주요 사용 시나리오 |
|---|---|---|---|---|---|
| ref | 양방향 | 응 | 응 | 응 | 읽거나/바꾸려고 변수 전달할 때 |
| out | 밖으로만 | 아니 | 아니 (값 할당 전까지) | 응 (무조건!) | 메서드에서 여러 값 반환할 때 |
| in | 안으로만 | 응 | 응 | 아니 | 큰 구조체를 "읽기 전용"으로 넘길 때 메모리 아끼려고 |
걱정하지 마, 이제 하나씩 다 볼 거야. 생각보다 훨씬 쉬워.
2. ref 수정자: 참조로 넘기기 - 읽기도, 쓰기도 가능
상상해봐: 친구한테 커피 한 잔을 가져다주면서 "마셔도 되고, 데워도 되고, 뭐든 추가해도 돼"라고 말하는 거야. 친구는 그 커피로 뭐든 할 수 있고, 남은 게 있든 없든 다시 너한테 돌아와. ref가 딱 이런 느낌이야.
void DoSomething(ref int x)
{
x = x + 10; // 입력 파라미터를 바꿔줌
}
ref로 파라미터를 넘기려면, 선언할 때도, 호출할 때도 이 수정자를 붙여야 해:
int myNumber = 5;
DoSomething(ref myNumber);
Console.WriteLine(myNumber); // 15 출력됨
무조건 변수여야 해. 참조로 식(expression)을 넘길 수 없어. 이런 코드는 안 돼:
DoSomething(ref 10); // 여기엔 변수여야 해!
- 호출 전: 변수는 반드시 초기화되어 있어야 해 (값이 할당되어 있어야 함).
- 메서드 내부: 읽기도, 값 바꾸기도 가능.
- 호출 후: 변경된 값이 그대로 남아있어.
예시: 변수 값 서로 바꾸기
가끔 두 변수의 값을 서로 바꿔야 할 때가 있어. 근데 여기서 중요한 점: C#에서 값 타입(예: int)은 기본적으로 값으로 전달돼. 즉, 메서드에 넘기면 값만 복사되고, 변수의 참조는 안 넘어가. 메서드가 호출한 쪽의 변수를 바꾸려면 ref 키워드를 써야 해.
ref 없이 예시 — 안 바뀜:
// 값 바꾸기 시도 — 안 바뀜
void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(x, y); // 값 복사만 됨
Console.WriteLine($"{x}, {y}"); // → 10, 20 — 아무것도 안 바뀜!
Swap 함수 안의 a와 b는 x와 y의 복사본이야. 복사본만 바뀌고, 원본은 그대로야.
ref로 동작하는 예시 — 변수 값이 바뀜
// ref로 변수 값 바꾸기
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 10;
int y = 20;
Swap(ref x, ref y); // 변수의 참조가 넘어감
Console.WriteLine($"{x}, {y}"); // → 20, 10 — 제대로 바뀜
ref는 변수의 참조를 넘겨주는 거야, 값이 아니라.
그래서 Swap 함수가 x와 y를 직접 바꿀 수 있어.
ref 언제 써?
- 넘겨준 변수를 바꿔야 할 때 (예: 값 증가, 교체 등).
- return으로 새 값을 반환할 필요 없이, 그냥 인자를 직접 바꾸고 싶을 때.
- 여러 값을 "쏟아내기" 용도로는 쓰지 마 — 그럴 땐 다른 수정자가 더 좋아.
3. out 수정자: 밖으로만 넘기기
이번엔 이런 상황: 친구 집에 갔는데 빈 컵을 들고 "이거 좀 채워줘! 커피든 차든 뭐든!"이라고 하는 거야. 친구는 무조건 뭔가를 채워줘야 하고, 안 그러면 컴파일러가 난리남.
out은 메서드가 반환값(return) 말고 여러 값을 한 번에 넘겨줄 때 쓰는 거야. 이런 파라미터는 메서드 안에서 반드시 값이 할당되어야 해.
void GetCoordinates(out int x, out int y)
{
x = 5;
y = 10; // 이거 없으면 에러!
}
- 호출 전: 변수는 초기화 안 해도 돼. 호출할 때 out int x처럼 바로 써도 됨.
- 메서드 내부: 반드시 값을 할당해야 해. 안 그러면 컴파일러가 파업함.
- 호출 후: 변수에 새 값이 들어가 있음.
예시: 메서드에서 두 값 한 번에 반환하기
void ParseNameAndAge(string input, out string name, out int age)
{
string[] parts = input.Split(','); // 문자열을 배열로 쪼갬 - ',' 기준으로 나눔
name = parts[0];
age = int.Parse(parts[1]);
}
string userInput = "이반,25";
ParseNameAndAge(userInput, out string userName, out int userAge);
Console.WriteLine($"{userName} — {userAge} 살");
out 언제 써?
- 메서드에서 여러 값을 반환해야 할 때 (예: 문자열을 여러 부분으로 나눌 때 등).
- 반환 타입이 미리 정해지지 않았을 때 (예: 문자열을 숫자로 변환 시도 등).
.NET에서 자주 쓰는 예시: int.TryParse(string, out int) — 완전 대표적이야!
string input = "123";
if (int.TryParse(input, out int parsedNumber))
{
Console.WriteLine($"변환됨: {parsedNumber}");
}
else
{
Console.WriteLine("변환 에러!");
}
int.TryParse는 문자열을 숫자로 변환할 수 있으면 true를 반환하고, 실패하면 false를 반환해. 실제 숫자 값은 out으로 넘겨줘.
ref와 out 쓸 때 흔한 실수와 웃긴 상황
- ref 쓸 때 변수 초기화 안 하면 컴파일러가 화냄.
- out에 값 할당 안 하면 컴파일러가 더 화냄.
- 호출할 때 수정자 빼먹으면 안 됨: DoSomething(x) 대신 DoSomething(ref x)처럼 써야 해.
- ref나 out을 상수나 리터럴에 쓰면 절대 안 됨! 무조건 변수여야 해. 변수만 메모리 주소가 있어서 참조를 만들 수 있거든.
4. in 수정자: 읽기 전용, 참조로만
가끔 메서드에 엄청 크고 복잡한 객체를 넘겨야 할 때가 있어. 보통 이런 객체는 참조로 넘기는데, 그러면 함수가 실수로 그 객체를 바꿔버릴 수도 있지 — 우리가 참조를 넘겼으니까.
물론 객체를 복사해서 넘기면 원본은 안 바뀌겠지만, 객체가 크면 쓸데없이 데이터만 복사하게 돼. 차라리 객체를 함수에 넘기면서, "이건 읽기만 해!"라고 컴파일러한테 시키는 게 훨씬 효율적이야.
in은 구조체를 참조로 넘기면서 읽기 전용으로만 쓸 수 있게 해주는 새로운 수정자야. 약간 "진귀한 검을 유리관에 전시"하는 느낌: 볼 수는 있지만, 만질 수는 없어.
void PrintPoint(in Point pt)
{
pt.X = 5; // 에러! 읽기만 가능.
Console.WriteLine($"포인트: {pt.X}, {pt.Y}"); //이건 가능
}
- 호출 전: 변수는 반드시 초기화되어 있어야 해.
- 메서드 내부: 읽기만 가능, 값 변경 불가.
- 메모리 절약: 구조체가 복사되지 않고 참조로만 넘어감.
예를 들어, 큰 구조체 배열을 넘길 때 불필요한 복사를 피하려고 써. 근데 클래스(참조 타입)에는 in이 별 효과 없어.
예시: 큰 구조체를 참조로 넘겨서 복사 방지
struct BigData
{
public int A, B, C, D, E;
}
void PrintBigData(in BigData data)
{
data.A = 10; // 안 됨 - 읽기 전용!
Console.WriteLine(data.A + data.B + data.C + data.D + data.E);
}
BigData myData = new BigData { A = 10, B = 20, C = 30, D = 40, E = 50 };
PrintBigData(in myData);
5. 자주 궁금해하는 질문들:
ref/out/in을 배열이나 참조 타입에도 쓸 수 있어?
당연하지! 근데 기억해: 배열 자체가 이미 참조 타입이야. ref로 배열을 넘기면 배열 참조 자체를 바꿀 수 있어. 보통은 구조체에 더 많이 써.
ref랑 out의 차이가 뭐야? 둘 다 값 받을 수 있잖아?
ref는 변수가 미리 초기화되어 있어야 하고, out은 안 그래도 돼. 대신 out은 메서드 안에서 무조건 한 번은 값 할당해야 해.
리터럴(예: 숫자 5)로 ref/out/in 쓰면?
컴파일러가 바로 에러 내. 무조건 변수만 가능!
ref/out/in 파라미터 몇 개까지 쓸 수 있어?
제한 없어! 근데 코드 읽기 힘들어지니까 2~3개 넘으면 튜플, 구조체, 클래스 반환을 고민해봐.
GO TO FULL VERSION