[C#] 구조체 vs 클래스

spixychz·2026년 5월 26일

기술면접

목록 보기
13/13

구조체 vs 클래스

핵심 요약 비교

특징구조체 (Struct)클래스 (Class)
타입 분류값 타입 (Value)참조 타입 (Reference)
저장 방식값 자체 저장참조 저장
메모리 위치지역 변수면 Stack, 배열이나 클래스 내부면 HeapHeap (참조하는 주소값만 스택에 할당)
GC대상 아님 (Stack : 스코프 종료시 즉시 해제)대상 (메모리 정리 시 오버헤드 발생)
기본 생성0 or falsenull
복사값 자체 복사얕은 복사
상속XO
null 가능XO

언제 무엇을 선택해야 할까

  • 구조체를 선택할 때
    • 데이터의 크기가 작을 때
      • 일반적으로 16바이트 이하
    • 논리적으로 단일값을 나타낼 때
      • ex) Vector3, Color
    • 상속 구조가 필요 없을 때
    • 불변성을 유지하는 데이터일 때
    • Cache Hit를 극대화해야할 때

메모리 할당과 라이프 사이클

  • 구조체
    • 메소드 내부나 지역 변수로 구조체를 생성하면 스택 메모리에 할당
    • 선언된 스코프를 벗어나는 순간 메모리에서 제거
      • 할당과 해제가 매우 빠르다
      • GC 개입 없음
  • 클래스
    • 실제 데이터는 힙 메모리에 할당되고, 스택에는 데이터를 가리키는 주소(참조값)만 저장
    • 아무도 이 객체를 참조하지 않게되면 메모리 제거
      • GC 작동

할당과 복사

  • 구조체
    • StructB = StructA
      • StructA가 가진 모든 데이터가 복사되어 StructB로 들어간다
      • 둘 중 하나를 수정해도 다른 하나에는 영향을 미치지 않는다
      • ref, in 으로 참조 전달 가능
  • 클래스
    • ClassB = ClassA
      • 실제 데이터가 복사되는 것이 아닌 힙 메모리의 주소값이 복사
      • ClassB의 값을 수정하면 ClassA의 값도 동일하게 변경

Boxing & Unboxing

  • 개념
    • Boxing : 값 타입을 참조 타입으로 암시적 변환하는 과정
    • Unboxing : 박싱되어 힙에 있는 객체를 다시 원래의 값 타입으로 명시적 변환하는 과정
  • 내부 동작
    • 박싱이 발생하면 데이터가 힙 영역으로 통째로 복사 및 할당
      • 원본 값 타입을 참조하는 것이 아니라 힙에 복사본 객체를 새로 생성
    • GC의 감시 대상으로 전환

배열 생성시 메모리 연속성과 Cache Hit

  • 구조체 배열의 연속성
    • 구조체 배열을 생성하면 배열 자체가 힙에 할당되더라도, 그 내부에 있는 실제 구조체 데이터들이 메모리상에 빈틈업이 연속적으로 할당
    • CPU는 메모리를 블록 단위로 읽어오기 때문에, 다음 데이터를 읽을 때 Cache Hit가 발생하여 처리 속도가 빠르다
  • 클래스 배열의 파편화
    • 클래스 배열을 생성하면 배열 안에는 힙 메모리의 주소들만 연속해서 나열되고, 실제 데이터들은 힙 영역에 흩어져서 할당
    • CPU가 배열을 순회할 때, 주소를 읽고 다시 힙의 위치로 점프해서 데이터를 가져와야 하므로 Cache Miss 발생하여 속도가 느리다

구조체

구조체와 인터페이스

struct Monster : IDamageable
{
		public void GetDamaged(int damage);
}

// -----

IDamageable monster = new Monster();    // 박싱 발생

// -----

Monster monster = new Monster();
monster.GetDamaged(10);                 // 박싱 없음

void Hit<T>(ref T damageable) where T : IDamageable
{
		damageable.GetDamaged(10);
}
  • 구조체는 값 타입인 반면 인터페이스는 참조 타입처럼 객체를 다룬다
  • 구조체가 인터페이스를 구현한 뒤, 이 구조체를 인터페이스 타입의 변수에 대입하거나, 인터페이스를 매개변수로 받으면 Boxing 발생
    • 원인 : 인터페이스 참조 변수는 힙 영역에 존재하는 객체의 메모리 주소를 가리키도록 설계
    • 해결 : Generic으로 박싱 회피 가능
    • 주의
      • 구조체가 인터페이스를 구현하는 것 자체는 Boxing이 아니다
      • 인터페이스 타입으로 변환되는 순간 Boxing이 발생한다

구조체의 방어적 복사

  • readonly struct
    • 모든 읽기 전용 필드
    • Immutable
  • ref
    • 읽기/쓰기 참조
  • in
    • 읽기 전용 참조

구조체의 null

  • 구조체의 한계
    • 구조체 변수는 실제 데이터 그 자체를 담는 상자
    • 변수 자체가 데이터를 포함하기 때문에 값 없음 상태를 표현할 수 없다
  • 클래스의 null
    • 클래스 변수는 힙 메모리의 주소를 담는 상자
    • 아무 주소도 없을 때 null
  • Nullable
    • T?
    • 스택 메모리에 null 포인터가 들어가는 것이 아닌 Nullable 구조체가 생성

Transform.position.x = 10; 은 왜 안될까 ?

  • 구조체 Property의 함정
    • position은 변수가 아닌 프로퍼티이며, 반환되는 Vector3가 구조체이기 때문
    • 과정
      • Get 호출
      • 값 복사
      • 값 변경 : 복사본의 값만 변경. 원본은 변함 없음
    • C# 컴파일러에서 에러 처리
  • 그럼 Vector3를 클래스로 만들면 ?
    • 정상적으로 작동
    • 하지만 수많은 Vector3 생성/복사 시 GC 최소화를 위해 구조체로 설계

Memory Padding

public struct structA
{
		public bool A;    // 1 바이트 + 패딩
		public int B;     // 4 바이트
		public bool C;    // 1 바이트 +  패딩
		                  // 총 12 바이트
}

public struct structB
{
		public int B;     // 4 바이트
		public bool A;    // 1 바이트	
		public bool C;    // 1 바이트 + 2 바이트 패딩
		                  // 총 8 바이트
}
  • CLR은 특정 크기의 데이터를 정렬된 주소에서 읽을 때 가장 효율적
    • 정렬을 맞추기 위해 Padding을 추가
    • 필드의 정렬 순서에 따라 구조체 크기가 달라질 수 있다
  • LayoutKind.Sequential
    • C# 구조체의 기본 설정
    • 개발자가 코드를 작성한 순서대로 메모리에 올린다
    • 정렬을 위한 padding이 삽입될 수 있다
  • [StructLayout(LayoutKind.Auto)]
    • CLR이 런타임 최적화를 위해 필드 순서 자동 조절 가능

ref struct

  • 스택 전용 구조체
  • 불가능 조건
    • 클래스의 필드 변수로 선언
    • 배열로 생성
    • 박싱
    • 인터페이스 구현
    • 코루틴 내부에서 사용 불가

클래스

숨겨진 메모리 오버헤드 : Object Header

  • 구조체의 무결성
    • int 2개를 가진 구조체는 메모리상에서 정확히 8 바이트만 차지
  • 클래스의 오버헤드
    • 클래스로 객체를 생성하면 선언한 필드 데이터 외에도 CLR이 객체를 관리하기 위해 암시적으로 덧붙이는 정보
    • SyncBlock, TypeHandle
    • 보통 16 바이트 내외

클래스는 힙에 올라가는데 그럼 MonoBehaviour는 ?

  • MonoBehaviour(UnityEngine.Object) 는 이중 구조로 메모리에 적재
  • C# 메모리와 C++ 본체
    • 유니티 엔진은 C++로 작성되어 있고, C# 스크립트는 그 위에서 돌아간다
    • C++ Native Memory
      • 유니티 엔진의 핵심 영역에 실제 데이터 할당
      • 게임 오브젝트의 Transform 정보, 물리 엔진 상태, 엔진 내부 콜백(Update, OnTriggerXXX …)
      • GC의 관리를 받지 않고 엔진이 직접 통제
    • C# Managed Heap
      • C# 환경(Mono / IL2CPP)의 힙 메모리 영역에 MonoBehaviour 객체 할당
      • 실제 데이터가 아닌 C++ 네이티브 메모리에 있는 Instance ID를 가리킨다
      • GC의 관리 대상
  • new MonoBehaviour() 를 사용할 수 없는 이유
    • new 키워드는 C# Managed Heap에 껍데기만 생성할 뿐, C++ 엔진 측에 진짜 본체를 만들어달라고 요청하지 않기 때문
    • AddComponent<T>()

Destroy 와 GC의 엇박자 : Fake Null

// Destroy 호출 이후

// C# 힙에는 아직 객체가 남아있으므로, 순수 C# Object 기준으로는 null이 아님.
object obj = myMonoBehaviour;
bool isObjectNull = (obj == null); // false (실제 C# 메모리에는 존재함)

// 하지만 유니티가 재정의한 == 연산자는 C++ 본체가 죽었는지 확인하여 true를 반환함.
bool isMonoNull = (myMonoBehaviour == null); // true (유니티의 Fake Null)
  • Destroy를 호출했을 때
    • C++ 메모리
      • Unity 엔진이 프레임 종료 시점에 C++ 네이티브 객체를 파괴하고 메모리 회수
    • C# 힙 메모리
      • C#에있는 껍데기 객체는 본체가 사라졌으므로 참조를 잃은 빈 껍데기가 되고, GC가 돌아갈 때까지 그대로 메모리 점유
profile
UNITY로 게임 개발하는 사람

0개의 댓글