← 문제로

3. 동적 할당 비용·단편화, 메모리 풀/오브젝트 풀

난이도 상
내 답안
모범답안

모범답안 — 동적 할당 비용·단편화, 메모리 풀/오브젝트 풀

난이도: 상

핵심 답변

  • 동적 할당의 비용: ① 자유 블록 탐색(자유 리스트/빈 슬롯 찾기, 분할·병합), ② 멀티스레드 락 경합(또는 스레드 로컬 캐시 관리), ③ 부족 시 OS로의 시스템 콜(brk/mmap)과 페이지 폴트, ④ 새로 받은 메모리의 캐시 콜드 상태, ⑤ 메타데이터(헤더) 오버헤드.
  • 내부 단편화: 요청보다 큰 블록을 줘서 블록 내부에 못 쓰는 공간이 생김(정렬/버킷 크기 반올림 때문).
  • 외부 단편화: 전체 빈 공간은 충분한데 흩어져 있어 큰 연속 블록을 못 주는 상태. 장기 운영·다양한 크기 할당/해제가 반복될 때 누적된다.
  • 메모리 풀/오브젝트 풀: 미리 큰 덩어리를 확보해 동일/유사 크기 객체를 슬롯 단위로 빌려주고 반납받는 구조. 할당/해제가 O(1) 포인터 조작이라 빠르고, 단편화를 억제하며, 객체 재사용으로 GC/생성 비용을 줄인다.

깊이 있는 설명 (메커니즘, 왜)

  • 범용 할당자의 동작: 다양한 크기 요청을 처리하려고 자유 리스트/빈(bin)/사이즈 클래스 등을 관리한다. 할당 시 적합한 블록 탐색, 해제 시 인접 블록과 병합(coalescing). 이 과정이 분기·메타데이터 갱신·락을 수반해 비결정적 지연(스파이크)을 만든다.
  • 왜 시간이 갈수록 느려지나: 다양한 수명의 객체가 섞여 할당/해제되면 빈 공간이 잘게 쪼개진다(외부 단편화). 할당자는 점점 더 긴 탐색을 하고, 큰 연속 공간 부족으로 OS에 추가 페이지를 요청 → RSS가 우상향. 누수가 없어도 메모리가 안 줄어드는 이유는, 해제된 공간이 할당자 내부엔 남아도 OS로 반납되지 않거나(반납 단위가 페이지/아레나 단위) 단편화로 반납 못 하기 때문이다.
  • 풀이 빠른 이유: 모든 슬롯이 같은 크기 → 탐색 불필요, 자유 슬롯을 단일 연결 리스트(또는 인덱스 스택)에서 pop/push만 하면 됨. 메모리가 연속이라 캐시 지역성도 좋다. 외부 단편화가 구조적으로 발생하지 않는다.
  • 트레이드오프: 풀은 메모리를 미리/계속 점유(선할당). 객체 종류마다 풀이 필요하고, 크기가 천차만별이면 비효율. 객체 재사용 시 상태 초기화(reset) 를 빠뜨리면 이전 데이터가 새 객체에 남는 버그가 난다.

응용/실무 연결 (게임서버에서)

시나리오 진단:

  • 생성/소멸 지연·스파이크 = 초당 수만 회의 동적 할당/해제로 인한 탐색·락·페이지 폴트 비용과 단편화 누적.
  • RSS 우상향 + 누수 없음 = 전형적 외부 단편화(또는 할당자가 OS에 메모리 반납을 안 함).

풀 설계 제안:

  • 고정 크기 풀(slab/free-list): 투사체처럼 크기·수명이 균일하고 대량인 객체는 고정 크기 풀이 최적. 슬롯 = sizeof(Projectile), 자유 슬롯 스택으로 O(1) 대여/반납.
  • 타입별 풀 분리: Monster, Projectile, Buff 등 종류별로 풀을 두어 크기 혼재를 방지.
  • 스레드 처리: 워커별 스레드 로컬 풀로 락 제거가 이상적. 크로스 스레드 반납이 필요하면 lock-free MPSC 큐로 반납하거나, 소유 스레드가 회수하도록 설계.
  • 풀 고갈 정책: ① 청크 단위로 동적 증설(grow by chunk, 기존 객체 주소는 유지), ② 상한을 두고 초과 시 일반 할당으로 폴백 또는 생성 거부(레이트 리밋). 운영 안정성을 위해 상한 + 모니터링 권장.
  • 재사용 시 reset: 반납 시 또는 대여 시 객체 상태를 명확히 초기화. C#이면 풀이 GC 압박까지 줄여준다(아래 문제 4와 연결).
  • 사전 워밍업: 서버 기동 시 예상 피크만큼 미리 확보해 런타임 페이지 폴트/증설 지연 제거.

흔한 오답·함정

  • "풀 쓰면 무조건 빠르다"고만 말하고 reset 누락·메모리 상시 점유·범용성 저하 같은 단점을 못 짚는 것.
  • 내부/외부 단편화를 혼동. (내부 = 블록 안 낭비, 외부 = 블록 사이 흩어짐)
  • RSS 증가를 곧바로 "메모리 누수"로 단정. 단편화/할당자 캐싱일 수 있다.
  • 멀티스레드에서 단일 전역 풀에 락을 걸어 오히려 경합 병목을 만드는 것.
  • 풀에서 빌린 객체의 포인터를 반납 후에도 들고 쓰는 use-after-return(댕글링) 버그.

꼬리질문 대비

  • Q. jemalloc/tcmalloc은 이 문제를 어떻게 줄이나요? A. 사이즈 클래스 + 스레드 로컬 캐시로 락 경합과 단편화를 줄이고, 큰 블록은 mmap으로 따로 다뤄 반납을 쉽게 한다. 범용이라 도메인 특화 풀보다는 일반적이지만 기본 할당자보다 게임 워크로드에 유리한 경우가 많다.
  • Q. 풀 객체에 가상 함수/다형성이 필요하면? A. 슬롯 크기를 가장 큰 파생 타입에 맞추거나 타입별 풀을 분리. placement new로 생성, 명시적 소멸자 호출 후 슬롯 반납.
  • Q. 외부 단편화를 근본적으로 없애는 방법은? A. 압축(compaction, 객체 이동 — 포인터 갱신 필요)이나 고정 크기 슬롯(슬랩). 게임 서버는 보통 압축이 어렵고 비용이 커서 고정 크기 풀로 회피한다.