[TIL/22] C# 메모리관리

안건우·2025년 10월 29일

sparta_til

목록 보기
21/26

1. Stack과 Heap의 차이

Stack (스택)

  • 관리 방식: 자동 메모리 관리 (LIFO 구조)
  • 저장 데이터: 값 타입(Value Type), 지역 변수, 메서드 호출 정보
  • 할당/해제: 컴파일 타임에 크기 결정, 스코프를 벗어나면 자동 해제
  • 성능: 매우 빠름
  • 크기: 제한적
  • 생명주기: 메서드 실행 동안만 유지

Heap (힙)

  • 관리 방식: GC(Garbage Collector)가 관리
  • 저장 데이터: 참조 타입(Reference Type) 객체
  • 할당/해제: 런타임에 동적 할당, GC가 주기적으로 해제
  • 성능: 상대적으로 느림
  • 크기: 크고 유연함
  • 생명주기: 참조가 없어질 때까지 유지

2. Struct

Struct는 값 타입(Value Type)의 사용자 정의 데이터 구조

주요 특징

  • Stack에 저장: 메모리 할당이 빠르고 GC 부담 없음(예외는 있는듯?)
  • 값 복사: 할당 시 전체 데이터가 복사됨
  • 상속 불가: 다른 struct나 class를 상속할 수 없음 (인터페이스는 구현 가능)
  • 작은 데이터에 최적: 일반적으로 16바이트 이하 권장라고하는데 그렇게 중요하지는 않다고?

언제 뭘 쓸까?

  • Struct 사용: 작은 데이터, 논리적으로 단일 값, 불변성, 자주 생성/소멸
  • Class 사용: 큰 데이터, 복잡한 동작, 상속 필요, 참조 공유 필요

3. Boxing과 Unboxing 그리고 피해야 하는 이유

Boxing

값 타입을 참조 타입(object)으로 변환하는 과정

int num = 123;           // Stack에 저장
object obj = num;        // Boxing: Heap에 새로운 객체 생성하고 값 복사

Unboxing

참조 타입을 값 타입으로 다시 변환하는 과정

object obj = 123;        // Boxing된 상태
int num = (int)obj;      // Unboxing: Heap에서 값을 읽어 Stack으로 복사

왜 피해야 하나?

1. 불필요한 Heap 할당

  • 값 타입이 Heap에 복사되어 메모리 낭비
  • GC의 작업 부담 증가

2. 성능 저하

// 나쁜 예: 반복문에서 Boxing
ArrayList list = new ArrayList();
for(int i = 0; i < 10000; i++)
{
    list.Add(i);  // 10000번의 Boxing 발생!
}

// 좋은 예: 제네릭 사용
List<int> list = new List<int>();
for(int i = 0; i < 10000; i++)
{
    list.Add(i);  // Boxing 없음
}

3. 추가 CPU 사이클

  • 메모리 할당, 복사, 타입 검사 등의 오버헤드

4. GC 압박

  • Boxing된 객체들이 Heap에 쌓여서 GC가 더 자주 동작

4. 제네릭(T)

제네릭은 타입 안정성과 메모리 효율성을 동시에 확보하기 위해 도입됨. 특히 값 타입 작업 시 Boxing/Unboxing을 제거해서 성능과 메모리 관리를 크게 개선함.

핵심 정리

  • Stack: 빠르고 자동 관리, 값 타입 저장
  • Heap: GC 관리, 참조 타입 저장, 상대적으로 느림
  • Struct: Stack 기반 값 타입, 작고 불변한 데이터에 적합
  • Boxing/Unboxing: Heap 할당과 GC 부담을 유발하므로 피해야 함
  • 제네릭(T): Boxing 제거와 타입 안정성으로 메모리 효율성 극대화

실무 팁

생성과 할당은 비싸다, 찾기는 조금 더 싸다

대부분의 상황에서 정답: 커넥션 풀링, 쓰레드 풀링, 오브젝트 풀링

→ 즉 메모리 관점에서도 new를 반복해서 사용하기보단 가능하다면 캐싱해서 사용하는 게 낫다.

예전에 형변환 귀찮을때 +"";를 참 애용했었는데...ㅎㅎ

이제야 StringUtil의 존재 이유를 알았다...ㅎㅎ

0개의 댓글