빠른 비교
// 값 타입 — 대입 시 복사
int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10 — a는 변하지 않음
// 참조 타입 — 대입 시 참조 공유
var first = new Player("Mina");
var second = first; // 같은 객체를 가리킴
second.Name = "Jin";
Console.WriteLine(first.Name); // "Jin" — first도 바뀜!갈리는 기준
어떤 타입이 값 타입이고 어떤 타입이 참조 타입인가
헷갈릴 때는 아래처럼 먼저 나눠 보면 됩니다.
int count = 1; // 값 타입
DateTime now = DateTime.Now; // 값 타입
string name = "Mina"; // 참조 타입
int[] scores = { 1, 2, 3 }; // 참조 타입
var player = new Player("M"); // 참조 타입- 값 타입:
int,double,bool,struct,enum,ValueTuple - 참조 타입:
class,string, 배열,List<T>, 인터페이스
값 타입 — 스택에 값 자체를 저장
값 타입은 변수 자체에 값이 저장됩니다. 스택에 할당되기 때문에 GC 부담이 없고, 대입 시 값 전체가 복사됩니다.
// 대표 값 타입
int, long, short, byte // 정수
float, double, decimal // 실수
bool, char // 논리, 문자
struct // 사용자 정의 값 타입
enum // 열거형
(int X, int Y) // ValueTuple
// 메서드에 전달 시 복사됨
void Double(int n) { n *= 2; }
int x = 5;
Double(x);
Console.WriteLine(x); // 5 — 원본 변화 없음참조 타입 — 힙에 객체, 스택에 참조
참조 타입은 실제 데이터가 힙에 저장되고, 변수에는 그 힙 주소(참조)만 저장됩니다. 대입하면 같은 힙 주소를 공유합니다.
// 대표 참조 타입
class, interface, record class // 사용자 정의
string, object // 내장 참조 타입
int[], List<T> // 배열, 컬렉션
// 메서드에 전달 시 참조 공유
void ChangeName(Player p) { p.Name = "Jin"; }
var player = new Player("Mina");
ChangeName(player);
Console.WriteLine(player.Name); // "Jin" — 같은 객체 수정됨// 값 타입 struct는 대입 시 복사
Point p1 = new Point(1, 2);
Point p2 = p1;
p2.X = 10;
Console.WriteLine(p1.X); // 1boxing/unboxing — 값 타입이 힙을 왕복하는 비용
boxing은 값 타입을 object 또는 인터페이스 타입으로 변환할 때 발생합니다. 스택의 값을 힙에 새 객체로 복사하므로 힙 할당 + GC 부담이 생깁니다.
unboxing은 그 반대입니다. object로 저장된 값을 다시 원래 값 타입으로 꺼낼 때 힙에서 스택으로 복사가 일어납니다. unboxing은 반드시 명시적 캐스트가 필요하고, 타입이 맞지 않으면 InvalidCastException이 발생합니다.
int n = 42;
object obj = n; // boxing — int 값을 힙에 새 객체로 복사
int m = (int)obj; // unboxing — 힙 객체에서 int 값을 꺼내 스택에 복사
// unboxing 타입 불일치 → 런타임 예외
object obj2 = 42; // int로 boxing됨
double d = (double)obj2; // ❌ InvalidCastException — long이나 float도 불가, int만 허용
// boxing이 발생하는 흔한 패턴
ArrayList list = new ArrayList();
list.Add(42); // ❌ int → object boxing 발생
// 제네릭으로 boxing 방지
List<int> generic = new List<int>();
generic.Add(42); // ✅ boxing 없음string — 참조 타입이지만 불변
string은 참조 타입이지만 불변(immutable) 입니다. 한 번 만들어진 string 객체는 변경할 수 없고, 수정 시 항상 새 객체가 생성됩니다.
string a = "hello";
string b = a;
a = a + " world"; // a는 새 객체 "hello world"를 가리킴
Console.WriteLine(b); // "hello" — b는 원래 객체 그대로
// 따라서 string 비교는 값 비교가 기본 동작 (==가 오버로드됨)
string s1 = "test";
string s2 = "test";
Console.WriteLine(s1 == s2); // true (값 비교)
Console.WriteLine(object.ReferenceEquals(s1, s2)); // true (인터닝으로 같은 객체일 수도)체크포인트
| 항목 | 값 타입 | 참조 타입 |
|---|---|---|
| 저장 위치 | 스택 (또는 다른 구조체 안) | 힙 |
| 대입 시 | 전체 복사 | 참조 공유 |
기본 비교 == | 값 비교 | 참조 비교 (오버로드 가능) |
| 대표 타입 | int, struct, enum | class, string, 배열 |
| null 허용 | 기본 불가 (int?로 허용) | 가능 |
주의할 점
string은 참조 타입이지만 불변입니다. "참조 타입 = 내부 값이 바뀔 수 있다"는 단순화는 string에서 틀립니다. s = s + "x" 처럼 string을 수정하는 것처럼 보이는 코드는 실제로 매번 새 string 객체를 만듭니다.
큰 struct를 메서드에 자주 전달하면 복사 비용이 쌓입니다. 이 경우 in 키워드로 읽기 전용 참조로 전달하거나, readonly struct로 선언해 컴파일러 최적화를 유도할 수 있습니다.
참고 링크
2 sources