이전 글에서 배열에 대해 다루다가 값 타입과 참조 타입에 대한 이야기가 나왔었다.
원래 이번 글에서는 함수에 대한 포스팅을 할 예정이었으나 값 타입(Value Type)과 참조 타입(Reference Type)에 엮인 내용이 많아 따로 짚고 넘어가는 것이 좋을 거 같다.
값 타입 / 참조 타입 복습
- 값 타입 (Value Type)
- bool, byte, int, double, decimal, char // struct, enum
- 대입 / 인수 전달시 값 자체가 복사
- 일반적으로 스택(Stack)에 저장되지만, 예외적으로 박싱(boxing)되거나 참조 타입의 필드로 존재하면 힙(Heap)에 배치될 수 있음.
- 참조 타입 (Reference Type)
- class, string, array, delegate, object
- 변수에는 객체 주소(참조)가 저장되고, 실제 객체는 보통 힙(Heap)에 있다.
- 대입 / 인수 전달 시 참조(주소)가 복사 ( = 같은 객체를 가리키는 주소 )
참고 : string은 참조 타입이지만 불변성 때문에 값 타입 처럼 다루는 느낌을 줌
스택(Stack) / 힙(Heap)
메모리 배치와 수명
- 스택 (Stack)
- 메서드 호출 시 생성되는 지역 변수/매개변수의 저장소
- 후입선출 구조 (LIFO: Last In First Out)
- 스코프가 끝나면 자동 해제
- 접근 비용이 낮고 빠름
- 힙 (Heap)
- 동적 할당 영역
- new로 생성된 객체의 실제 데이터 저장
- GC(Garbage Collector)가 접근 불가능해진 객체를 수거
- 동적 할당/해제로 유연하지만 관리 비용 존재
값 복사 / 참조 복사
값 타입을 넘기면 값 자체가 복사 된다
참조 타입을 넘기면 참조 주소가 값처럼 복사되어 같은 원본을 가리킨다.
말로만 하면 이해가 잘 되지 않을 수 있으니 코드로 살펴보자.
값 타입의 경우
struct Point { public int X, Y; }
void MoveX(Point p) // p는 호출 시 '값 복사본'
{
p.X += 10; // 복사본만 변경
}
var p0 = new Point { X = 1, Y = 2 };
MoveX(p0); // p0.X == 1 (원본 변경 없음)
참조 타입의 경우
class Person { public string Name; }
void Rename(Person p)
{
p.Name = "Bob"; // 같은 객체의 필드 변경 → 호출자에 반영됨
}
void Reassign(Person p)
{
p = new Person { Name = "Carol" }; // 매개변수 복사본의 참조만 바뀜
}
var a = new Person { Name = "Alice" };
Rename(a); // a.Name == "Bob"
Reassign(a); // a.Name == "Bob" (여전히 같은 객체)
배열(Array)은 참조 타입
void ZeroFirst(int[] arr)
{
arr[0] = 0; // 같은 배열 인스턴스의 요소 변경 → 호출자에 반영
}
void ReassignArray(int[] arr)
{
arr = new int[] { 9, 9, 9 }; // 매개변수 참조만 교체, 호출자에 영향 없음
}
int[] xs = { 1, 2, 3 };
ZeroFirst(xs); // xs == [0, 2, 3]
ReassignArray(xs); // xs == [0, 2, 3]
string은 참조 타입이지만 불변
void Upper(string s)
{
s = s.ToUpperInvariant(); // 새 문자열 생성, 매개변수 참조만 변경
}
string t = "hello";
Upper(t); // t == "hello" (원본 불변)
문자열을 수정하려면 새 문자열을 반환하거나 StringBuilder를 사용하는 등 다른 방식을 생각해보아야 한다.
얕은 복사 (Shallow) / 깊은 복사 (Deep)
얕은 복사 : 객체의 필드 값을 1단계만 복사. 필드가 참조 타입이면 참조(주소)만 공유.
깊은 복사 : 객체가 가진 중첩 참조까지 새 인스턴스를 만들어 완전 독립 복제함
얕은 복사 예시
class Address { public string City; }
class Person
{
public string Name;
public Address Addr;
// MemberwiseClone()은 protected이므로 클래스 내부에서만 호출 가능
public Person ShallowCopy() => (Person)this.MemberwiseClone();
}
var p1 = new Person { Name = "Alice", Addr = new Address { City = "Seoul" } };
var p2 = p1.ShallowCopy(); // 얕은 복사
p2.Name = "Bob";
p2.Addr.City = "Busan";
// 결과
// p1.Name == "Alice" (값 타입/문자열 참조 자체는 교체되어 독립)
// p1.Addr.City == "Busan" (중첩 참조(Address)는 공유 → 내부 변경이 반영)
깊은 복사 예시
class Address { public string City; }
class Person
{
public string Name;
public Address Addr;
public Person DeepCopy()
{
return new Person
{
Name = this.Name, // string은 불변 → 복사로 충분
Addr = new Address { City = this.Addr.City } // 중첩 참조를 '새로' 생성
};
}
}
var p1 = new Person { Name = "Alice", Addr = new Address { City = "Seoul" } };
var p2 = p1.DeepCopy();
p2.Addr.City = "Busan";
// 결과
// p1.Addr.City == "Seoul" (완전 독립 복제)
배열, 컬렉션에서의 복사
Array.Clone(), Array.Copy(...), new List<T>(old) 는 요소를 대입해서 채움
요소가 값 타입이면 사실상 깊은 복사와 동일한 효과
요소가 참조 타입이면 얕은 복사
class Node { public int V; }
var a1 = new[] { new Node{ V = 1 }, new Node{ V = 2 } };
var a2 = (Node[])a1.Clone(); // 얕은 복사(배열 인스턴스만 새 것, 요소 참조는 공유)
a2[0].V = 99;
// a1[0].V == 99 (요소 객체는 동일 참조)
var a3 = a1.Select(n => new Node { V = n.V }).ToArray(); // 깊은 복사
a3[0].V = 7;
// a1[0].V == 99 (완전 독립)
박싱 (Boxing) / 언박싱 (Unboxing)
박싱 (Boxing) : 값 타입을 참조 타입으로 힙에 포장하는 것
언박싱 (Unboxing) : 박싱된 참조에서 원래 값 타입으로 캐스팅하는 것
int n = 10;
object boxed = n; // Boxing (값 타입 → 힙 객체)
int m = (int)boxed; // Unboxing (참조 → 값 타입)
박싱이 발생하는 경우
1. 비제네릭 컬렉션에 값 타입을 넣을 때
var list = new System.Collections.ArrayList();
list.Add(42); // Boxing
int x = (int)list[0]; // Unboxing
2. 값 타입을 object/인터페이스 파라미터로 넘길 때
interface IHasArea { double Area(); }
struct Rect : IHasArea
{
public double W, H;
public double Area() => W * H;
}
IHasArea r = new Rect { W = 3, H = 4 }; // 인터페이스 변수에 담는 순간 Boxing
double a = r.Area();
불필요한 박싱은 힙 할당 + GC 압박 + 캐스팅 비용을 유발한다고 하니 사용시 주의하자.
이렇게 참조타입과 관련된 키워드 몇가지에 대해 간단히 알아봤다.
개인적으로 나머지는 다 어렵지 않은 내용 같은데 박싱/언박싱 개념이 잘 와닿지 않는 것 같다.
이후 기회가 된다면 다시 자세히 다루어 보도록 하자.
'C#' 카테고리의 다른 글
| C# - 배열 심화 문제 - 2차원 배열 swap (0) | 2025.09.11 |
|---|---|
| C# - 함수 (Function) (0) | 2025.09.10 |
| C# - 배열 ( Array ) (0) | 2025.09.08 |
| C# - 콘솔로 별 찍기 (다이아몬드 편) (0) | 2025.09.08 |
| C# - 콘솔로 별 찍기 (삼각형 편) (0) | 2025.09.04 |