C#에서 값 타입과 참조 타입은 대입, 전달, 복사 시 동작이 다릅니다. 코드의 버그와 성능 문제를 피하려면 둘의 차이를 정확히 이해해야 합니다.
1. 핵심 요약
값 타입(struct, enum 등)은 대입 시 실제 데이터가 복사됩니다. 참조 타입(class, array, string, delegate, record class 등)은 대입 시 참조(주소)가 복사되어 같은 객체를 가리킵니다. 따라서 값 타입은 독립적, 참조 타입은 공유된 상태를 갖습니다.
2. 대입(=) 시 복사 동작
값 타입은 값 복사, 참조 타입은 참조 복사입니다.
// 값 타입: 값이 복사됩니다.
int a = 10;
int b = a;
b = 20;
Console.WriteLine($"{a}, {b}"); // 10, 20
// 참조 타입: 참조(주소)가 복사됩니다.
class Box { public int V; }
var r1 = new Box { V = 10 };
var r2 = r1; // 같은 객체를 가리킵니다.
r2.V = 20;
Console.WriteLine($"{r1.V}, {r2.V}"); // 20, 20struct도 사용자 정의 값 타입이므로 같은 규칙이 적용됩니다. class는 참조 타입입니다.
3. 메서드 인자 전달: by value vs ref/out/in
C#은 기본적으로 매개변수를 값으로 전달합니다. 값 타입은 값이 복사되고, 참조 타입은 참조가 복사되어 같은 객체를 공유합니다. 참조 자체를 바꾸려면 ref를 사용합니다.
class Foo { public int V; }
static void Mutate(Foo f) { f.V = 99; } // 같은 객체의 상태를 변경
static void Reassign(Foo f) { f = new Foo { V = 0 }; } // 지역 참조만 교체
static void ReassignRef(ref Foo f) { f = new Foo { V = 0 }; } // 호출자 참조 교체
var f = new Foo { V = 1 };
Mutate(f);
Console.WriteLine(f.V); // 99
Reassign(f);
Console.WriteLine(f.V); // 99 (그대로)
ReassignRef(ref f);
Console.WriteLine(f.V); // 0 (새 객체)값 타입은 ref/out/in을 사용하지 않으면 복사본이 전달되어 내부 변경이 호출자에 반영되지 않습니다. in은 읽기 전용 참조 전달이라 변경이 금지됩니다.
4. 배열과 컬렉션 복사
배열과 대부분의 컬렉션(List 등)은 참조 타입입니다. 변수 대입은 참조만 복사하므로 요소 변경이 공유됩니다. 내용을 복사하려면 별도 메서드를 사용합니다.
var a1 = new int[] { 1, 2, 3 };
var a2 = a1; // 참조 복사
a2[0] = 9;
Console.WriteLine(a1[0]); // 9
// 내용 복사(얕은 복사)
var a3 = new int[a1.Length];
Array.Copy(a1, a3, a1.Length);
a3[0] = 7;
Console.WriteLine(a1[0]); // 9 (독립)List<T>의 생성자(List<T>(IEnumerable<T>))로 복사하면 얕은 복사입니다. 참조 요소를 깊게 복사하려면 각 요소를 새로 생성해야 합니다.
5. 얕은 복사 vs 깊은 복사
얕은 복사는 객체 자체만 복사하고 내부 참조는 공유합니다. 깊은 복사는 내부 참조까지 새로 생성합니다.
class Node
{
public int V;
public Node? Next;
public Node CloneShallow() => (Node)this.MemberwiseClone(); // 얕은 복사
}
var n1 = new Node { V = 1, Next = new Node { V = 2 } };
var n2 = n1.CloneShallow();
n2.Next!.V = 9;
Console.WriteLine(n1.Next!.V); // 9 (내부 참조 공유)깊은 복사는 생성자를 통해 내부까지 새로 만들어야 합니다. 직렬화 기반 복사나 수동 복사 패턴을 고려합니다.
6. string/record와 불변(Immutable)
string은 참조 타입이지만 불변입니다. 대입은 참조를 복사하지만 문자열 조작은 새 인스턴스를 만듭니다.
string s1 = "ab";
string s2 = s1; // 참조 복사
s2 += "c"; // 새 문자열 생성
Console.WriteLine(s1); // ab
Console.WriteLine(s2); // abcrecord class는 참조 타입, record struct는 값 타입입니다. record의 with 식은 얕은 복사를 수행합니다. 내부 참조가 있다면 별도 깊은 복사가 필요합니다.
7. 박싱/언박싱과 복사
값 타입을 object나 인터페이스로 변환하면 박싱이 발생해 값이 힙에 복사됩니다. 언박싱 시에도 값이 다시 복사됩니다. 빈번한 박싱은 성능에 영향을 줍니다.
int v = 42;
object o = v; // 박싱: 값이 객체로 복사
int v2 = (int)o; // 언박싱: 값이 다시 복사8. struct vs class 선택 가이드
작고 불변이며 값 의미론이 필요한 경우에만 struct를 사용합니다. 대부분은 class가 적합합니다. struct가 클수록 복사 비용이 커지므로 주의합니다. 변경을 호출자에 반영해야 한다면 ref/out/in 사용을 고려합니다.
9. 실무 체크포인트
대입이 값 복사인지 참조 복사인지 확인합니다. 배열/컬렉션은 내용을 복사해야 독립됩니다. 메서드에서 참조 자체를 바꾸려면 ref를 사용합니다. 얕은 복사와 깊은 복사의 차이를 의도적으로 선택합니다. string과 기타 불변 타입은 참조가 같아도 변경 부작용이 없습니다. 박싱을 유발하는 API 사용을 줄여 성능을 지킵니다.
'C#' 카테고리의 다른 글
| C# 메서드 반환 타입으로 Func<T> 사용하기 (0) | 2026.06.24 |
|---|---|
| C# 가변 인자(params)와 성능 이슈 (0) | 2026.06.24 |
| C# ViewModel과 모델 변환 로직 설계 (0) | 2026.06.23 |
| C# 동적 프로퍼티 생성 및 바인딩 (0) | 2026.06.23 |
| C# 코드 분석 도구(Roslyn Analyzer) 제작하기 (0) | 2026.06.22 |