C#에서 대부분의 메서드는 메서드에 제공해야 할 데이터를 정의하는 매개변수(파라미터)를 0개 이상 가질 수 있습니다. 메서드를 호출하는 코드는 반드시 해당 데이터를 인자(argument) 형태로 메서드에 전달해야 합니다. 즉, 메서드는 자신의 입력값을 매개변수로 선언하고, 호출하는 쪽에서는 인자를 통해 값을 공급하는 구조입니다.
예를 들어 다음 메서드와 그 호출 코드를 살펴보겠습니다.
static void Greet(string greeting){
Console.WriteLine(greeting);
}
...
Greet("Hello");위 예제에서 greeting은 Greet() 메서드의 매개변수이고, "Hello"는 메서드에 전달되는 인자입니다.
값으로 전달(Pass by Value)의 기본 동작
메서드를 호출하며 인자를 전달할 때 기본적으로 값으로 전달(pass by value)됩니다. 이는 메서드로 전달되는 순간 값의 복사본이 생성된다는 의미입니다. 따라서 메서드 내부에서 인자에 가한 변경 사항은 원본 변수에는 전혀 반영되지 않습니다.
using System;
int number = 8;
Increase(number);
Console.WriteLine(number); // 출력: 8
static void Increase(int num){
num = num + 1;
Console.WriteLine(num); // 출력: 9
}
반면 객체처럼 참조 타입 변수를 전달할 때는 변수가 실제 객체가 아닌 참조(reference)를 담고 있기 때문에, C#은 여전히 참조 자체를 값으로 복사합니다. 하지만 복사된 참조 변수 역시 메모리상의 동일한 객체를 가리키므로, 메서드 내부에서 객체에 가한 변경 사항은 메서드 외부의 변수에도 그대로 나타납니다.
using System;
var john = new User{
Name = "John",
Salary = 50000
};
Promote(john);
Console.WriteLine(john.Salary); // 출력: 60000
static void Promote(User u){
u.Salary += 10000;
Console.WriteLine(u.Salary); // 출력: 60000
}
그러나 메서드 내부에서 변수 자체의 값을 변경하면 이야기가 다릅니다. 변경된 것은 어디까지나 복사본일 뿐 실제 객체가 아니므로, 그 변화는 메서드 밖에 반영되지 않습니다. 예를 들어 메서드 안에서 인자에 null을 할당해도 실제 변수는 그대로 유지됩니다.
using System;
var john = new User{
Name = "John",
Salary = 50000
};
Promote(john);
Console.WriteLine(john.Salary); // 출력: 50000
static void Promote(User u){
u = null;
}
C#은 이러한 매개변수 전달 방식을 개발자가 직접 제어할 수 있도록 세 가지 한정자(modifier) 키워드를 제공합니다.
ref 한정자: 참조로 전달하기
C#에서는 ref 한정자를 사용해 매개변수를 참조로 전달할 수 있습니다. 전달되는 변수가 참조 타입이든 값 타입이든 관계없이, 매개변수와 인자는 동일한 메모리 위치를 가리키게 됩니다.
다음 예제에서 인자 u에 null을 설정하면 john 변수 역시 null이 되어 NullReferenceException이 발생합니다.
예제
using System;
var john = new User{
Name = "John",
Salary = 50000
};
Promote(ref john);
Console.WriteLine(john.Salary); // System.NullReferenceException 발생
static void Promote(ref User u){
u = null;
}
주의: ref 한정자로 전달되는 인자는 반드시 전달하기 전에 초기화되어 있어야 합니다.
out 한정자: 함수 밖으로 값 꺼내기
out 한정자는 ref와 유사하지만 다음과 같은 중요한 차이가 있습니다.
인자는 함수에 들어가기 전에 초기화될 필요가 없습니다.
인자는 함수에서 빠져나오기 전에 반드시 초기화(할당)되어야 합니다.
아래 예제에서 Hire 함수는 out 한정자를 통해 전달받은 새 사용자 객체를 초기화합니다. 호출 시점에 john 변수가 즉석에서 선언되는 점도 주목할 만합니다.
using System;
Hire(out User john);
Console.WriteLine(john.Salary); // 출력: 50000
static void Hire(out User u){
u = new User{
Name = "John",
Salary = 50000
};
}
ref 한정자와 마찬가지로 out 한정자가 붙은 변수도 참조로 전달됩니다.
실무에서는 out 변수를 활용해 하나의 함수에서 여러 반환 값을 얻는 패턴이 자주 사용됩니다.
using System;
var john = new User{
Name = "John",
Salary = 50000
};
bool shouldPromote = Raise(john.Salary, out double hike);
Console.WriteLine(shouldPromote); // True
Console.WriteLine($"인상 금액 = {hike}"); // 출력: 5000
static bool Raise(int salary, out double hike){
hike = salary * 0.1;
return hike < 7000;
}
in 한정자: 읽기 전용 참조 전달
in 한정자는 ref, out 매개변수와 유사하지만, in 인자를 받는 메서드는 해당 인자의 값을 수정할 수 없다는 결정적인 차이가 있습니다. 수정을 시도하면 C# 컴파일러가 컴파일 타임 오류를 발생시킵니다.
in 매개변수는 컴파일러가 큰 크기의 값 타입 메모리를 매개변수 변수로 복사하는 비용을 줄여주는 동시에, 객체가 실수로 변경되는 것도 방지해 줍니다. 따라서 크기가 큰 값 타입(struct)을 메서드에 전달할 때 성능과 안전성 면에서 특히 유용합니다.
var point = new Coord();
Verify(point);
static void Verify(in Coord c){
// 오류: 'in Coord'는 읽기 전용 변수이므로 할당할 수 없습니다.
c = new Coord();
}
struct Coord{
public int X;
public int Y;
}
세 가지 한정자 비교 요약
| 한정자 | 호출 전 초기화 필요 여부 | 메서드 내부에서 값 변경 |
|---|---|---|
| ref | 필요 | 가능 |
| out | 불필요 | 필수 (반드시 할당해야 함) |
| in | 필요 | 불가 (읽기 전용) |
종합 예제
using System;
class Program{
static void Main(){
int number = 8;
Increase(number);
Console.WriteLine(number); // 출력: 8
var john = new User { Name = "John", Salary = 50000 };
Promote(john);
Console.WriteLine(john.Salary); // 출력: 60000
Leave(john);
Console.WriteLine(john.Salary); // 출력: 60000
LeaveByRef(ref john);
Console.WriteLine(john?.Salary); // 아무것도 출력하지 않음
User dave;
Hire(out dave);
Console.WriteLine(dave.Salary); // 출력: 50000
}
static void Increase(int num){
num = num + 1;
Console.WriteLine(num); // 출력: 9
}
static void Promote(User u){
u.Salary += 10000;
Console.WriteLine(u.Salary); // 출력: 60000
}
static void Leave(User u){
u = null;
}
static void LeaveByRef(ref User u){
u = null;
}
static void Hire(out User u){
u = new User{
Name = "John",
Salary = 50000
};
}
}
class User{
public string Name { get; set; }
public int Salary { get; set; }
}
실행 결과
9
8
60000
60000
60000
50000