[OS] 메모리 단편화(Fragmentation)와 배치 전략 - 12

2026. 4. 3. 14:39운영체제(OS)

메모리 단편화(Fragmentation)와 배치 전략

메모리 단편화의 종류(내부/외부), 해결 방법(공백 합병, 압축), 그리고 빈 공간에 프로세스를 배치하는 전략(최초·최적·최악 적합)을 정리한다. 현대 OS에서 페이징1이 이 문제를 어떻게 바꿔놓았는지, 그리고 Unity의 Managed Heap에서 동일한 문제가 왜 여전히 유효한지까지 함께 다룬다.

💡 핵심: 단편화는 메모리가 할당/해제를 반복하면서 사용 불가능한 빈 공간이 생기는 현상이다. 현대 OS는 페이징으로 외부 단편화를 구조적으로 해결했지만, Unity의 Managed Heap에서는 비압축 GC 특성 때문에 동일한 외부 단편화가 현재진행형이다.

1. 단편화란?

다중 프로그래밍 시스템에서 고정 분할 할당이나 가변 분할 할당 기법을 사용하면, 프로세스가 할당되고 해제되는 과정에서 메모리 곳곳에 사용하지 못하는 빈 공간이 발생한다. 이 현상을 단편화(Fragmentation)라 한다.

단편화에는 내부 단편화외부 단편화 두 종류가 있다. 둘 다 메모리 낭비를 유발하지만, 발생 위치와 심각성이 완전히 다르다. 내부 단편화는 할당된 블록 "안쪽"에서 남는 공간 문제이고, 외부 단편화는 블록들 "사이"에 빈 공간이 흩어지는 문제다.

내부 단편화 (Internal Fragmentation)

메모리를 고정 크기 블록으로 미리 나눠놓았을 때 발생한다. 프로세스가 블록보다 작으면, 할당은 되지만 블록 안쪽에 사용하지 못하는 공간이 남는다. 예를 들어 100KB 파티션에 70KB 프로세스를 넣으면 30KB가 낭비된다.

이 30KB는 해당 블록 내부에 갇혀 있어서 다른 프로세스가 사용할 수 없다. 하지만 프로세스가 종료되면 블록 전체가 해제되면서 자연스럽게 사라진다는 점에서, 치명적인 문제는 아니다.

▼ 100KB 파티션에 70KB 프로세스 할당
프로세스 A
70KB 사용
낭비
30KB

→ 30KB는 블록 내부에 갇혀 다른 프로세스가 사용 불가. 프로세스 종료 시 블록 전체가 해제되면 자연히 회복된다.

현대 페이징 환경에서도 내부 단편화는 동일하게 발생한다. 페이지 크기가 4KB인데 프로세스가 13KB를 요청하면, 4개 페이지(16KB)가 할당되고 마지막 페이지에서 3KB가 낭비된다. 다만 최대 손실이 한 페이지 미만(≈4KB)이므로 실무에서는 무시 가능한 수준이다.

외부 단편화 (External Fragmentation)

프로세스들이 할당과 해제를 반복하면서, 빈 공간이 메모리 여기저기에 흩어지는 현상이다. 빈 공간의 총합은 충분한데 연속된 큰 블록이 없어서, 새 프로세스를 적재할 수 없는 상태가 된다.

내부 단편화와 달리, 외부 단편화는 프로세스가 해제되면 오히려 빈 구멍이 더 많아져서 상황이 악화될 수 있다. "메모리가 남아있는데 할당 자체가 불가능한" 상황을 만들기 때문에, 시스템 크래시로 직결될 수 있는 치명적인 문제다.

▼ STEP 1 — 프로세스 4개 적재
A
30KB
B
20KB
C
30KB
D
20KB
▼ STEP 2 — A, C 종료 → 빈 공간 60KB (30+30)
빈 공간
30KB
B
20KB
빈 공간
30KB
D
20KB
▼ STEP 3 — 50KB 프로세스 적재 시도 → 실패!
30KB
부족
B
30KB
부족
D
새 프로세스
50KB 필요

→ 총 빈 공간 60KB인데 연속 공간이 없어 50KB 프로세스를 넣지 못한다.

구분내부 단편화외부 단편화
발생 위치할당된 블록 안쪽블록과 블록 사이
원인블록이 프로세스보다 큼해제된 빈 공간이 흩어짐
자연 회복프로세스 종료 시 해소해제가 오히려 악화시킴
심각성낮음 (약간의 낭비)높음 (할당 불가 → 크래시)

2. 단편화 해결 방법

공백 합병 (Coalescing Holes)

인접해 있는 빈 공간들을 하나로 합쳐서 더 큰 연속 공간을 만드는 방법이다. 프로세스가 해제될 때 양옆의 빈 공간을 확인하고, 빈 공간끼리 붙어 있으면 자동으로 합쳐주는 방식으로 동작한다. 추가적인 프로세스 중단 없이 수행할 수 있다는 장점이 있다.

❌ BEFORE
[빈 30KB] [빈 20KB]
→ 50KB 할당 불가
✅ AFTER (합병)
[빈 50KB]
→ 50KB 할당 가능

다만 공백 합병은 인접한 빈 공간끼리만 합칠 수 있다. 사용 중인 프로세스가 사이에 껴 있으면 합병이 불가능하다. 위의 외부 단편화 예시처럼 B, D가 사이에 있는 경우에는 합병만으로 해결할 수 없고, 압축이 필요하다.

압축 (Compaction)

프로세스들을 메모리의 한쪽 끝으로 밀어 모아서, 흩어진 빈 공간을 하나의 큰 덩어리로 만드는 방법이다. 교재에서는 기억장소의 집약이라고도 표현하며, 기억장소의 쓰레기 수집(Garbage Collection)이라고도 부른다.

압축 과정에서 프로세스들의 물리 주소가 변경되기 때문에, 기준 레지스터와 경계 레지스터를 사용해 주소를 재배치해야 한다. 또한 압축 도중에는 모든 프로세스를 멈춰야 하므로 다른 처리를 할 수 없다는 큰 단점이 있다.

프로세스 중지
메모리 재배치
Base/Boundary 갱신
재개

3. 메모리 배치 전략

가변 분할 할당 방식에서는 프로세스가 들어갈 수 있는 빈 공간이 여러 개 존재할 수 있다. 이때 어디에 배치할 것인지를 결정하는 전략이 배치 전략이다. 교재의 예시처럼 8KB 프로세스를 배치할 때, 사용 가능한 공간이 5KB, 9KB, 10KB, 10KB, 20KB로 흩어져 있다면 각 전략의 선택이 달라진다.

(1) 최초 적합 (First-fit)

주기억장치의 빈 공간을 순서대로 검색하여, 프로세스가 들어갈 수 있는 첫 번째 공간에 배치하는 방식이다. 검색은 처음부터 시작하거나, 지난 번 검색이 끝난 곳에서부터 이어서 수행한다(next-fit).

전체를 탐색하지 않아도 되므로 속도가 빠르다는 장점이 있지만, 메모리 앞쪽에 작은 단편화가 집중적으로 쌓인다는 단점이 있다. 앞쪽에 작은 공간들이 많아지면 적합한 공간을 찾는 데 시간이 점점 늘어난다.

(2) 최적 적합 (Best-fit)

사용 가능한 빈 공간들 중에서 프로세스를 넣었을 때 남는 공간이 가장 작은 곳을 선택하는 방식이다. 가장 딱 맞는 곳에 넣으므로 메모리 활용도가 높아 보이지만, 전체 공간을 탐색해야 하므로 속도가 느리다.

또한 할당 후 남는 공간이 매우 작아서(자투리), 다른 프로세스가 사용하기 어려운 아주 작은 외부 단편화가 많이 생긴다는 역설적인 문제가 있다. 크기 순으로 정렬되지 않은 목록에서는 최적의 공간을 찾기 위해 전체를 검색해야 한다.

(3) 최악 적합 (Worst-fit)

사용 가능한 빈 공간 중에서 가장 큰 공간을 선택하는 방식이다. 남는 공간이 크기 때문에 다른 프로세스가 그 공간을 재활용할 가능성이 높다는 것이 장점이다.

하지만 이 역시 전체 탐색이 필요하고, 큰 공간을 계속 소비하기 때문에 정작 큰 프로세스가 들어와야 할 때 공간이 부족해질 수 있다.

▼ 8KB 프로세스 배치 — 각 전략의 선택
사용중
5K
사용중
10K
9K
사용중
10K
20K
전략선택 공간이유
최초 적합④ (10K)첫 번째로 8K 이상인 공간
최적 적합⑤ (9K)8K를 넣으면 남는 공간이 가장 작음 (1K)
최악 적합⑧ (20K)가장 큰 공간을 선택

4. 페이징 — 외부 단편화의 구조적 해결

위에서 다룬 배치 전략과 압축은 모두 연속 메모리 할당 방식의 한계를 보완하기 위한 것이었다. 프로세스를 연속된 물리 메모리에 넣어야 한다는 전제 자체가 외부 단편화의 근본 원인이었기 때문이다.

현대 OS는 이 전제를 깨버렸다. 물리 메모리를 고정 크기 프레임(Frame)으로, 논리 메모리를 같은 크기의 페이지(Page)로 나눈 뒤, 비어 있는 아무 프레임에 페이지를 넣으면 된다. 프로세스가 연속된 물리 메모리를 가질 필요가 없어진 것이다. MMU2가 하드웨어적으로 가상 주소를 물리 주소로 변환해주기 때문에, 프로세스 입장에서는 여전히 연속된 메모리처럼 보인다.

페이징이 해결한 것
  • 외부 단편화 완전 제거 — 고정 크기 프레임 단위 할당이므로, 빈 프레임만 있으면 어디든 할당 가능
  • 배치 전략 불필요 — First-fit / Best-fit / Worst-fit 구분이 의미 없음
  • Compaction 불필요 — 프로세스를 밀어줄 필요 자체가 사라짐
남아있는 문제

내부 단편화는 여전히 존재한다. 마지막 페이지가 4KB를 다 채우지 못하면 나머지가 낭비된다. 다만 최대 손실이 페이지 크기 - 1바이트(≈4KB)이므로 실무에서는 문제가 되지 않는 수준이다.

🕰 연속 할당 시대
👨‍💻 OS 개발자
"빈 공간 중에 어디에 넣지? First-fit? Best-fit? 구멍 생기면 Compaction 돌려야 하는데 그 동안 전부 멈춰..."
// 50KB 프로세스 적재 시도
[30KB 빈] [B 사용중] [30KB 빈] [D 사용중]
// → 총 60KB 비었는데 연속 50KB가 없어서 실패
// → Compaction 수행... 전체 중단...
🤔 "프로세스를 연속된 메모리에 넣어야 한다는 전제 자체를 없애면?"
✅ 페이징 도입 이후
👨‍💻 현대 OS
"4KB 페이지 단위로 쪼개서 빈 프레임 아무 데나 넣으면 됨. 배치 전략? 필요 없음. Compaction? 필요 없음."
// 13KB 프로세스 → 4KB 페이지 4개
Page 0 → Frame 7   // 물리적으로 흩어져 있어도
Page 1 → Frame 2   // 프로세스는 연속으로 인식
Page 2 → Frame 15
Page 3 → Frame 9   // (3KB만 사용, 1KB 내부 단편화)

5. Unity Managed Heap — 외부 단편화의 부활

OS 레벨에서 페이징으로 해결된 외부 단편화가, Unity의 Managed Heap에서는 현재진행형이다. OS의 가상 메모리 관리와 애플리케이션 내부의 힙 메모리 관리는 별개의 계층이기 때문이다.

Unity의 Managed Heap은 Boehm GC3를 사용한다. 이 GC는 비세대(Non-generational)이라서 힙 전체를 스캔하고, 비압축(Non-compacting)이라서 수거 후에도 객체를 재배치하지 않는다. 교재에서 배운 압축(Compaction)이 바로 "객체를 한쪽으로 밀어서 빈 공간을 합치는 것"이었는데, Boehm GC는 이걸 하지 않는 것이다.

결과적으로, 교재에서 봤던 외부 단편화 시나리오가 Unity Managed Heap 안에서 그대로 재현된다. 할당/해제가 반복되면 힙에 구멍이 숭숭 뚫리고, 큰 객체를 넣을 연속 공간이 없으면 GC가 돌아간다. 그래도 안 되면 힙 자체가 확장된다. 문제는 확장된 힙이 앱 종료 전까지 OS에 반환되지 않는다는 것이다.

할당/해제 반복
힙에 구멍
연속 공간 부족
GC 수행
힙 확장 (미반환)

이것이 "힙 할당을 줄여라"라는 가이드라인의 진짜 이유다. 단순히 GC가 돌 때 프레임이 끊기는 단기적 문제만이 아니라, 외부 단편화 → 힙 확장 → 메모리 부족 → OOM 크래시라는 장기적 문제로 이어지기 때문이다. Object Pooling은 할당/해제 사이클 자체를 끊어서 GC 스파이크 감소와 단편화 예방을 동시에 달성하는 전략이다.

⚠️ .NET CLR과의 차이: 일반 .NET CLR의 GC는 세대별(Generational) + 압축(Compacting) 방식이라 객체를 재배치해서 외부 단편화를 해소한다. Unity가 Boehm GC를 사용하는 이유는 게임의 실시간 특성상 압축 비용이 너무 크기 때문이다.
// Object Pooling — 할당/해제 사이클을 끊어 단편화 예방
public class BulletPool
{
    private Queue<Bullet> _pool = new Queue<Bullet>();

    public Bullet Get()
    {
        return _pool.Count > 0
            ? _pool.Dequeue()
            : new Bullet();  // 최초 1회만 힙 할당
    }

    public void Return(Bullet bullet)
    {
        bullet.Reset();
        _pool.Enqueue(bullet);  // 해제 대신 풀에 반환 → 구멍 안 생김
    }
}

📎 용어 설명

  1. 페이징(Paging) — 물리 메모리를 고정 크기 프레임으로, 논리 메모리를 같은 크기 페이지로 나누어 비연속적으로 할당하는 메모리 관리 기법
  2. MMU(Memory Management Unit) — CPU 내부에서 가상 주소를 물리 주소로 변환해주는 하드웨어 장치
  3. Boehm GC — Unity가 사용하는 가비지 컬렉터. 비세대·비압축 방식으로, 힙 전체를 스캔하며 객체 재배치를 수행하지 않는다
OS 메모리 관리 단편화 Fragmentation 배치 전략 페이징 Unity Boehm GC Object Pooling