10. 질문 — 언어별 메모리 관리 비교: C++ RAII/이동 vs C# GC (종합)
난이도 최상내 답안
모범답안
모범답안 — 언어별 메모리 관리: C++ RAII/이동 vs C# GC (종합)
난이도: 최상
핵심 답변
**C++**은 수명을 개발자가 소유권으로 명시한다 — RAII(자원을 객체 수명에 묶음), 이동 의미론(복사 없이 소유권 이전), RVO(반환값 복사 제거)로 결정론적 해제와 무복사 성능을 얻는다. **C#**은 GC가 도달 불가능한 객체를 자동 회수한다 — 세대별 mark-sweep-compact로 짧게 사는 객체를 싸게 처리하지만, GC가 도는 순간 지연(스톱)이 생긴다. 게임서버에서 C++은 *수명 버그(dangling/누수)*를, C#은 *GC 압박(할당량)*을 가장 조심해야 한다.
깊이 있는 설명
C++ — RAII / 이동 / RVO
- RAII: 자원(메모리, 락, 소켓)을 객체 생성자에서 획득하고 소멸자에서 해제 → 스코프를 벗어나면 자동·결정론적 정리. 예외 안전성의 기반.
- 이동 의미론:
std::move로 큰 객체(벡터, 버퍼)를 복사 없이 소유권만 넘김 → 깊은 복사 비용 제거. - RVO/NRVO: 함수가 객체를 반환할 때 컴파일러가 복사/이동조차 생략하고 호출자 자리에 직접 생성.
- 소유권을 타입으로:
unique_ptr(단독 소유),shared_ptr(공유 소유),weak_ptr(관찰)로 "누가 해제 책임을 지는가"를 코드로 강제 → dangling/이중 해제/누수 방지.
C# — GC
- 세대별: Gen0(새 객체, 자주·빠르게 수집) → Gen1 → Gen2(오래 산 객체, 드물게·비싸게). 대부분 객체는 Gen0에서 금방 죽는다는 가정.
- mark-sweep-compact: 도달 가능한 객체 표시 → 나머지 회수 → 살아남은 것 압축(단편화 제거, 참조 갱신).
- 서버 GC vs 워크스테이션 GC: 서버 GC는 코어마다 힙·GC 스레드를 두어 처리량↑(게임서버 권장). 워크스테이션 GC는 지연 우선·단일 힙.
- 백그라운드 GC: Gen2 수집을 애플리케이션과 병행해 스톱 시간을 줄임.
핀닝 / LOH
- 핀닝(pinning): 네이티브 IO 등에 넘길 객체를 GC가 못 옮기게 고정 → 압축을 방해해 단편화 유발. 오래 핀하지 말 것.
- LOH(Large Object Heap, ≥85,000B): 큰 객체는 압축을 (기본) 안 해서 외부 단편화 발생, 수집도 Gen2와 함께라 비쌈. 큰 버퍼는 풀링/재사용.
응용/실무 연결
- C# 게임서버 GC 스파이크 줄이기: 핫 루프 할당 제거(문자열·박싱·LINQ·클로저 회피), 버퍼는
ArrayPool, 작은 값 타입은struct,Span<T>로 무할당 처리, 서버 GC + 백그라운드 GC 설정. - C++ 서버: 소유권 규칙 문서화(
shared_ptr=소유,weak_ptr=관찰), 오브젝트 풀, 이동으로 버퍼 전달, 순환참조는weak_ptr로 차단.
흔한 오답·함정
- C#: "GC가 알아서 하니 신경 끄자" → 핫 루프 할당이 누적되면 틱마다 GC 스파이크. 할당량이 핵심 지표.
- C++:
shared_ptr남발 → 참조 카운트 원자 연산 비용 + 순환참조 누수. 단독 소유면unique_ptr. - "RVO 믿고 무조건 값 반환" — 대부분 최적화되지만, 조건 분기로 여러 객체를 반환하면 RVO가 안 될 수 있다.
꼬리질문 대비
- Q. GC가 "stop-the-world"인 이유? 표시/압축 중 객체 그래프가 바뀌면 안 되므로 일부 구간은 앱을 멈춘다. 백그라운드 GC가 이 시간을 줄임.
- Q. C++엔 GC가 없는데 누수는 어떻게 잡나? RAII + 스마트포인터 + 도구(ASan/LSan, valgrind).
- Q. C#에서
IDisposable/using은 GC와 뭐가 다른가? GC는 메모리 회수,Dispose는 비메모리 자원(파일/소켓/네이티브 핸들)의 결정론적 해제. GC 타이밍에 맡기면 안 되는 자원에 사용.