← 문제로

3. struct 값 의미론, 박싱, 가변 구조체의 함정

난이도 상
내 리뷰 · C#
해설 · C#

해설 — struct 값 의미론, 박싱, 가변 구조체의 함정

난이도: 상

답변 프레임워크: 요약 → 핵심 개념 → 단계별 내부 동작 → 흔한 오해·함정 → 면접 포인트

요약

예상 출력:

a.X=1, b.X=2
arr[0].X=11
list[0].X=20
c.X=101, boxed.X=100
h.P.X=6
  • b = a값 복사b.IncX()a 에 영향 없음 → a.X=1, b.X=2.
  • 배열 원소 arr[0]변수(addressable)arr[0].IncX() 가 원본을 바꿈 → 11.
  • list[0] 인덱서는 복사본을 반환tmp 를 바꿔도 리스트는 그대로 → 20.
  • boxed = c 시점에 복사가 힙으로 들어감. 이후 c.IncX() 는 c 만 101, 박스는 100.
  • h.P 는 클래스 필드라 변수h.P.X=5IncX()6.

핵심 개념

  • 값 타입 복사 의미론: struct 는 대입·인자 전달·반환 시 비트 단위 복사. 두 변수는 독립적. (class 는 참조 복사라 같은 객체를 공유.)
  • "원소가 변수냐 복사냐":
    • 배열 T[]: 인덱싱 결과는 변수(lvalue)arr[i].field = v, arr[i].Mutate() 가 원본을 바꾼다.
    • List 인덱서: this[int]값을 반환(rvalue 복사). list[i].field = v컴파일 에러(CS1612, "반환값을 수정할 수 없음"), list[i].Mutate()조용히 복사본만 바꾸고 버린다(무효과). 바꾸려면 var t = list[i]; t.Mutate(); list[i] = t;.
  • 박싱(boxing): 값 타입을 object/인터페이스로 변환하면 힙에 복사본(박스)이 생긴다. 언박싱((Point)boxed)도 복사를 꺼낸다. 그래서 박스와 원본은 독립적이고, 인터페이스를 통해 mutate 하면 박스만 바뀐다(원본 아님) — 가변 구조체의 대표적 혼란.
  • 가변 구조체 안티패턴: 위처럼 "어디서 복사가 일어나는지"가 보이지 않아 직관과 다른 결과를 낳는다. 권장은 불변(readonly) struct.

단계별 내부 동작

  1. b = a → 스택에 a 의 비트 복사. b.IncX() 는 b 만.
  2. arr[0].IncX() → 배열 원소는 변수. 메서드가 그 자리(원소)를 직접 mutate → 11.
  3. var tmp = list[0] → 인덱서가 복사본 반환, tmp 에 복사. tmp.IncX() 는 tmp 만 → list[0] 은 20. (list[0].IncX() 를 직접 써도 동일하게 무효과; list[0].X=5 는 컴파일 에러.)
  4. object boxed = c → X=100 인 c 가 힙 박스로 복사. c.IncX() → c.X=101. (Point)boxed 는 박스(100)의 복사를 반환 → 100.
  5. h.P.X = 5 → h(참조형)의 필드 P 는 변수라 직접 수정. h.P.IncX() → 6.

흔한 오해·함정

  • "메서드를 불렀으니 바뀌었겠지": List/프로퍼티/인덱서가 반환한 struct 는 복사본일 수 있다. mutate 가 조용히 사라진다.
  • 박싱 비용/누수: 값 타입을 object/비제네릭 컬렉션/인터페이스로 다루면 박싱 발생 → 힙 할당·GC 압박. 게임서버 핫패스에서 IComparable/object 경유 박싱을 조심(제네릭 제약 where T : struct/IEquatable<T> 로 회피).
  • 큰 struct 의 복사 비용: 값 복사가 잦으면 오히려 느리다. 큰 데이터는 class 또는 in/ref 전달, readonly struct/ref struct(Span) 고려.
  • mutable struct + 컬렉션/프로퍼티 조합이 버그의 온상 → 불변 struct + "변경은 새 값 반환(with 식/record struct)".

면접 포인트

  1. 값/참조 의미론과 "어디서 복사가 일어나는가"(대입·전달·반환·인덱서·박싱).
  2. 배열 인덱싱은 변수, List 인덱서는 복사 반환 — list[i].field=... 가 컴파일 에러인 이유 (CS1612), 우회 패턴.
  3. 박싱의 메커니즘·비용과 회피(제네릭/제약), 가변 구조체가 안티패턴인 이유, readonly/ref struct 활용.