🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

메모리 관리 기초 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 28. · 3 Views

메모리 관리 기초 완벽 가이드

운영체제가 메모리를 어떻게 관리하는지 기초부터 차근차근 알아봅니다. 논리 주소와 물리 주소의 차이부터 단편화 문제까지, 메모리 관리의 핵심 개념을 실무 스토리와 함께 쉽게 설명합니다.


목차

  1. 논리 주소 vs 물리 주소
  2. 주소 바인딩 시점
  3. 동적 로딩과 동적 링킹
  4. 연속 메모리 할당
  5. 고정 분할 vs 가변 분할
  6. 단편화 (내부/외부)

1. 논리 주소 vs 물리 주소

김개발 씨는 C 언어로 포인터를 공부하다가 문득 궁금해졌습니다. "printf로 출력한 이 주소값, 진짜 RAM의 주소일까?" 옆자리 박시니어 씨에게 물어보니 의외의 대답이 돌아왔습니다.

"그건 진짜 주소가 아니야."

논리 주소는 프로그램이 사용하는 가상의 주소이고, 물리 주소는 실제 RAM 칩에서 사용하는 진짜 주소입니다. 마치 아파트 호수와 실제 GPS 좌표의 관계와 같습니다.

이 구분을 이해하면 운영체제가 여러 프로그램을 동시에 실행하는 비밀을 알 수 있습니다.

다음 코드를 살펴봅시다.

public class AddressExample {
    public static void main(String[] args) {
        // 논리 주소: 프로그램이 보는 주소 (0x1000)
        int[] array = new int[100];

        // 이 주소는 논리 주소입니다
        System.out.println("배열 참조: " + array);

        // MMU가 논리 주소를 물리 주소로 변환
        // 논리 주소 0x1000 -> 물리 주소 0x5000 (예시)

        // 각 프로세스는 독립된 논리 주소 공간을 가짐
        // 프로세스 A의 0x1000 != 프로세스 B의 0x1000
    }
}

김개발 씨는 입사 3개월 차 주니어 개발자입니다. 오늘 C 언어 복습을 하다가 포인터 변수의 주소를 출력해봤습니다.

화면에 "0x7ffd5e8c3a40" 같은 숫자가 나왔는데, 이게 정말 RAM 어딘가에 있는 실제 위치인 걸까요? 박시니어 씨가 웃으며 설명을 시작했습니다.

"그건 논리 주소야. 진짜 RAM 주소인 물리 주소와는 달라." 그렇다면 논리 주소와 물리 주소의 차이는 정확히 무엇일까요?

쉽게 비유하자면, 논리 주소는 마치 아파트 동호수와 같습니다. "101동 1502호"라고 말하면 그 아파트 단지 안에서는 누구나 찾아갈 수 있습니다.

하지만 전 세계적으로 보면 그 주소만으로는 위치를 알 수 없습니다. 실제 위치를 알려면 GPS 좌표 같은 물리적인 주소가 필요합니다.

컴퓨터에서도 마찬가지입니다. 프로그램이 사용하는 주소는 그 프로그램만의 가상 세계에서 유효한 논리 주소입니다.

실제 RAM 칩의 어느 위치에 저장되어 있는지를 나타내는 것이 물리 주소입니다. 그런데 왜 이렇게 복잡하게 두 가지 주소를 사용할까요?

옛날 컴퓨터에서는 프로그램이 직접 물리 주소를 사용했습니다. 문제는 여러 프로그램을 동시에 실행할 때 발생했습니다.

프로그램 A가 주소 1000번을 쓰고 있는데, 프로그램 B도 1000번을 쓰려고 하면 충돌이 일어났습니다. 마치 두 사람이 같은 주차 공간에 차를 대려는 것과 같았습니다.

바로 이 문제를 해결하기 위해 논리 주소 개념이 등장했습니다. 각 프로그램에게 독립된 가상의 주소 공간을 제공하는 것입니다.

프로그램 A도 0번부터 시작하고, 프로그램 B도 0번부터 시작합니다. 하지만 실제로는 RAM의 서로 다른 위치에 저장됩니다.

이 변환을 담당하는 하드웨어가 바로 **MMU(Memory Management Unit)**입니다. MMU는 CPU와 메모리 사이에 위치해서 모든 메모리 접근을 감시합니다.

프로그램이 논리 주소 1000번에 접근하려고 하면, MMU가 재빨리 "이 프로그램의 1000번은 실제로 50000번이야"라고 변환해줍니다. 이 모든 과정이 하드웨어 수준에서 일어나기 때문에 프로그램은 자신이 가짜 주소를 쓰고 있다는 사실조차 모릅니다.

실제 현업에서는 이 개념이 어떻게 활용될까요? 가장 대표적인 예가 바로 가상 메모리입니다.

4GB RAM을 가진 컴퓨터에서 8GB짜리 게임을 실행할 수 있는 이유가 바로 여기에 있습니다. 논리 주소 공간은 물리 메모리보다 클 수 있고, 운영체제가 필요한 부분만 RAM에 올리고 나머지는 디스크에 보관합니다.

주의할 점도 있습니다. 디버깅할 때 보이는 주소값은 논리 주소이므로, 실제 메모리 덤프를 분석할 때는 물리 주소로 변환해야 합니다.

이 차이를 모르면 엉뚱한 곳을 찾다가 시간을 낭비할 수 있습니다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"그래서 여러 프로그램이 동시에 돌아가도 서로 방해하지 않는 거군요!" 논리 주소와 물리 주소의 분리는 현대 운영체제의 근간이 되는 개념입니다. 이를 이해하면 가상 메모리, 메모리 보호, 프로세스 격리 등 더 깊은 주제로 나아갈 준비가 된 것입니다.

실전 팁

💡 - 디버거에서 보이는 주소는 논리 주소이므로 물리 주소와 혼동하지 마세요

  • 각 프로세스는 독립된 논리 주소 공간을 가지므로 같은 주소값이라도 다른 위치를 가리킵니다

2. 주소 바인딩 시점

김개발 씨가 Java 프로그램을 작성하다가 궁금증이 생겼습니다. "컴파일할 때 메모리 주소가 정해지나요, 아니면 실행할 때 정해지나요?" 박시니어 씨가 화이트보드에 세 가지 시점을 그리기 시작했습니다.

주소 바인딩은 논리 주소가 물리 주소로 연결되는 시점을 말합니다. 컴파일 시점, 로드 시점, 실행 시점의 세 가지가 있습니다.

마치 집 주소를 정하는 것처럼, 언제 정하느냐에 따라 유연성이 달라집니다.

다음 코드를 살펴봅시다.

// 1. 컴파일 타임 바인딩 (Compile Time Binding)
// 주소가 컴파일 시 고정됨 - 임베디드 시스템에서 사용
// 물리 주소: 0x1000 고정

// 2. 로드 타임 바인딩 (Load Time Binding)
// 프로그램 로드 시 주소 결정
// 재배치 가능 코드 생성, 로드 시 실제 주소로 변환

// 3. 실행 타임 바인딩 (Execution Time Binding)
// 실행 중에도 주소 변경 가능 - 현대 OS가 사용
public class ModernProgram {
    // JVM이 실행 중 메모리 위치를 동적으로 관리
    private Object data;  // 실행 중 이동 가능

    // MMU + 재배치 레지스터가 실시간 변환 수행
}

김개발 씨는 운영체제 수업 시간에 졸다가 중요한 내용을 놓쳤습니다. "주소 바인딩이 뭐였더라..." 다행히 박시니어 씨가 점심시간에 친절하게 설명해주기로 했습니다.

"주소 바인딩은 쉽게 말해서 논리 주소와 물리 주소를 연결하는 시점이야." 비유하자면, 이사를 생각해봅시다. 새 집 주소를 언제 정하느냐에 따라 상황이 달라집니다.

집을 지을 때부터 주소가 정해져 있을 수도 있고, 이사 당일에 정해질 수도 있고, 심지어 살면서도 주소가 바뀔 수 있습니다. 첫 번째는 컴파일 타임 바인딩입니다.

프로그램을 컴파일할 때 물리 주소가 고정되는 방식입니다. 옛날 MS-DOS 시절에 많이 사용했습니다.

프로그램이 항상 메모리의 특정 위치에서만 실행됩니다. 마치 "이 프로그램은 무조건 0x1000번지에서 시작해야 해"라고 못 박아두는 것과 같습니다.

장점은 단순하고 빠르다는 것입니다. 하지만 단점이 치명적입니다.

그 위치에 다른 프로그램이 있으면 실행할 수 없습니다. 지금도 일부 임베디드 시스템에서 사용하지만, 일반 PC에서는 거의 사용하지 않습니다.

두 번째는 로드 타임 바인딩입니다. 프로그램을 메모리에 올릴 때 주소가 결정됩니다.

컴파일러는 재배치 가능 코드를 생성합니다. 이 코드는 "시작 위치로부터 100바이트 떨어진 곳"처럼 상대적인 주소를 사용합니다.

로더가 프로그램을 메모리에 올릴 때 실제 시작 주소를 더해서 최종 물리 주소를 계산합니다. 컴파일 타임 바인딩보다 유연하지만, 한 번 로드되면 위치를 바꿀 수 없다는 한계가 있습니다.

세 번째는 실행 타임 바인딩입니다. 현대 운영체제가 사용하는 방식입니다.

프로그램이 실행되는 중에도 메모리 위치가 바뀔 수 있습니다. 마치 살고 있는 집의 주소가 갑자기 바뀌어도 우편물이 제대로 오는 것과 같습니다.

이것이 가능한 비결은 MMU와 재배치 레지스터입니다. CPU가 논리 주소를 내보내면, MMU가 재배치 레지스터의 값을 더해서 물리 주소를 만듭니다.

운영체제가 재배치 레지스터 값만 바꾸면 프로그램 전체가 다른 위치로 이동합니다. Java나 Python 같은 현대 언어를 사용할 때, 가비지 컬렉터가 객체를 메모리 이곳저곳으로 옮기는 것도 실행 타임 바인딩 덕분입니다.

프로그램은 논리 주소만 알면 되고, 물리 주소는 런타임에서 알아서 처리합니다. 박시니어 씨가 정리했습니다.

"결국 유연성과 복잡성의 트레이드오프야. 현대 시스템은 복잡하더라도 유연성을 선택한 거지." 김개발 씨는 그제야 왜 현대 운영체제가 복잡한 메모리 관리를 하는지 이해했습니다.

실전 팁

💡 - 현대 애플리케이션 개발자는 대부분 실행 타임 바인딩 환경에서 작업합니다

  • 임베디드 시스템 개발 시에는 컴파일 타임 바인딩도 여전히 중요합니다

3. 동적 로딩과 동적 링킹

김개발 씨가 만든 프로그램의 실행 파일 크기가 100MB나 됩니다. "이걸 전부 메모리에 올려야 하나요?" 박시니어 씨가 고개를 저었습니다.

"필요한 것만 올리면 돼. 그게 바로 동적 로딩이야."

동적 로딩은 프로그램의 일부분을 필요할 때만 메모리에 올리는 기법입니다. 동적 링킹은 라이브러리를 실행 시점에 연결하는 방식입니다.

두 기법 모두 메모리를 효율적으로 사용하기 위한 전략입니다.

다음 코드를 살펴봅시다.

// 동적 로딩 개념 (Java 리플렉션 활용)
public class DynamicLoadingExample {
    public void processImage() {
        // 이미지 처리가 필요할 때만 해당 클래스 로드
        try {
            Class<?> imageProcessor = Class.forName(
                "com.example.ImageProcessor"  // 필요시에만 로드
            );
            Object processor = imageProcessor.newInstance();
            // 메모리 절약: 사용하지 않으면 로드되지 않음
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 자주 쓰는 기능은 이미 메모리에 있음
    // 드물게 쓰는 기능은 호출 시 로드
}

김개발 씨는 사내 ERP 시스템을 유지보수하고 있습니다. 이 프로그램은 회계, 인사, 재고, 영업 등 수십 개의 모듈로 구성되어 있습니다.

그런데 대부분의 직원은 자기 업무에 해당하는 모듈 하나만 사용합니다. "전체 프로그램을 메모리에 올리면 RAM이 부족하지 않을까요?" 박시니어 씨가 고개를 끄덕였습니다.

"맞아. 그래서 동적 로딩을 사용해." 동적 로딩은 마치 도서관과 같습니다.

도서관에 10만 권의 책이 있지만, 열람실에는 내가 빌린 책 몇 권만 놓여 있습니다. 필요할 때 서고에서 책을 가져오고, 다 읽으면 반납합니다.

열람실 책상(메모리)은 한정되어 있지만, 도서관 전체(프로그램 전체)의 책을 이용할 수 있습니다. 동적 로딩을 사용하면 프로그램의 일부만 메모리에 올라갑니다.

사용자가 회계 모듈을 클릭하면 그때 회계 모듈이 로드됩니다. 인사 모듈은 인사팀 직원이 클릭하기 전까지 메모리에 올라오지 않습니다.

이 기법의 큰 장점은 메모리 사용량 감소입니다. 오류 처리 코드, 잘 사용하지 않는 기능 등은 필요할 때만 로드됩니다.

특히 대용량 프로그램에서 효과적입니다. 그렇다면 동적 링킹은 무엇일까요?

링킹은 여러 개의 코드 조각을 하나로 연결하는 과정입니다. 정적 링킹은 컴파일 시점에 라이브러리 코드를 실행 파일에 복사해 넣습니다.

반면 동적 링킹은 실행 시점에 라이브러리를 연결합니다. Windows의 DLL(Dynamic Link Library)이나 Linux의 SO(Shared Object) 파일이 동적 링킹의 대표적인 예입니다.

여러 프로그램이 같은 라이브러리를 공유해서 사용할 수 있습니다. 동적 링킹의 장점은 두 가지입니다.

첫째, 실행 파일 크기가 작아집니다. 라이브러리 코드가 실행 파일에 포함되지 않기 때문입니다.

둘째, 라이브러리 업데이트가 쉽습니다. 라이브러리 파일만 교체하면 모든 프로그램에 적용됩니다.

하지만 주의할 점도 있습니다. "DLL 지옥"이라는 유명한 문제가 있습니다.

여러 프로그램이 같은 DLL의 다른 버전을 필요로 할 때 충돌이 발생합니다. 현대에는 이를 해결하기 위해 버전 관리나 가상 환경을 사용합니다.

김개발 씨가 물었습니다. "Java는 어떤가요?" 박시니어 씨가 대답했습니다.

"Java는 클래스 로더가 동적 로딩을 담당해. 필요한 클래스를 그때그때 로드하지.

리플렉션으로 직접 제어할 수도 있어." 동적 로딩과 동적 링킹은 메모리 효율성을 높이는 핵심 기법입니다. 현대 운영체제와 런타임 환경은 이 기법들을 자동으로 활용하고 있습니다.

실전 팁

💡 - 자주 사용하는 기능은 정적으로, 드물게 사용하는 기능은 동적으로 로드하는 것이 효율적입니다

  • 동적 라이브러리 의존성 문제를 예방하려면 버전 관리를 철저히 하세요

4. 연속 메모리 할당

김개발 씨가 운영체제 강의 영상을 보다가 질문이 생겼습니다. "프로세스를 메모리에 올릴 때 연속된 공간에 올려야 하나요?" 박시니어 씨가 대답했습니다.

"예전에는 그랬어. 연속 메모리 할당이라고 하지."

연속 메모리 할당은 각 프로세스를 메모리의 연속된 공간에 배치하는 방식입니다. 마치 주차장에서 차 한 대가 여러 칸을 연속으로 차지하는 것과 같습니다.

단순하지만 메모리 낭비 문제가 발생합니다.

다음 코드를 살펴봅시다.

// 연속 메모리 할당 시뮬레이션
public class ContiguousAllocation {
    private int[] memory = new int[1000];  // 1000 단위 메모리

    // 프로세스에게 연속된 메모리 할당
    public int allocate(int processId, int size) {
        int start = findContiguousSpace(size);
        if (start == -1) return -1;  // 연속 공간 없음

        // 연속된 공간에 프로세스 배치
        for (int i = start; i < start + size; i++) {
            memory[i] = processId;
        }
        return start;  // 시작 주소 반환
    }

    // First-Fit: 첫 번째로 맞는 공간 찾기
    private int findContiguousSpace(int size) {
        // 연속된 빈 공간 탐색 로직
        return 0;  // 시작 위치 반환
    }
}

김개발 씨는 메모리 관리의 역사를 공부하고 있습니다. 현대 운영체제의 복잡한 메모리 관리 기법을 이해하려면, 먼저 초기의 단순한 방식을 알아야 합니다.

연속 메모리 할당은 가장 직관적인 메모리 관리 방식입니다. 쉽게 비유하자면, 대형 마트의 주차장을 생각해봅시다.

일반 승용차는 주차 공간 하나면 됩니다. 하지만 대형 트럭은 여러 칸을 연속으로 차지해야 합니다.

트럭을 세 칸에 걸쳐 주차한다고 할 때, 1번과 3번 칸에 나눠서 주차할 수는 없습니다. 반드시 연속된 1-2-3번 칸이 필요합니다.

연속 메모리 할당도 마찬가지입니다. 100KB 크기의 프로세스는 메모리의 연속된 100KB 공간에 올라가야 합니다.

50KB씩 두 군데로 나눌 수 없습니다. 그러면 빈 공간이 여러 개 있을 때, 어디에 프로세스를 배치해야 할까요?

이에 대한 세 가지 전략이 있습니다. 첫째, **First-Fit(최초 적합)**입니다.

메모리를 처음부터 검색해서 프로세스가 들어갈 수 있는 첫 번째 공간에 배치합니다. 검색 시간이 짧다는 장점이 있습니다.

둘째, **Best-Fit(최적 적합)**입니다. 프로세스 크기와 가장 비슷한 공간에 배치합니다.

남는 공간을 최소화할 수 있지만, 전체 메모리를 검색해야 해서 시간이 오래 걸립니다. 셋째, **Worst-Fit(최악 적합)**입니다.

가장 큰 빈 공간에 배치합니다. 남은 공간이 커서 다른 프로세스가 들어갈 가능성이 높습니다.

어떤 전략이 가장 좋을까요? 연구에 따르면 First-Fit과 Best-Fit이 비슷한 성능을 보이고, Worst-Fit은 대체로 성능이 떨어집니다.

하지만 연속 메모리 할당의 근본적인 문제가 있습니다. 시간이 지나면 메모리에 작은 빈 공간들이 흩어지게 됩니다.

총 빈 공간은 충분한데도 연속된 공간이 없어서 새 프로세스를 올리지 못하는 상황이 발생합니다. 이것이 바로 외부 단편화 문제입니다.

다음 장에서 자세히 알아보겠습니다. 박시니어 씨가 덧붙였습니다.

"현대 운영체제는 연속 할당의 한계를 극복하기 위해 페이징이나 세그멘테이션을 사용해. 하지만 연속 할당의 개념을 알아야 왜 그런 기법이 필요한지 이해할 수 있지."

실전 팁

💡 - First-Fit은 구현이 간단하고 성능도 괜찮아서 실무에서 자주 사용됩니다

  • 연속 할당의 한계를 이해하면 페이징의 필요성을 자연스럽게 이해할 수 있습니다

5. 고정 분할 vs 가변 분할

김개발 씨가 박시니어 씨에게 물었습니다. "연속 메모리 할당에서 메모리를 미리 나눠놓는 방법도 있나요?" 박시니어 씨가 고개를 끄덕였습니다.

"응, 고정 분할 방식이 그래. 반대로 가변 분할 방식도 있어."

고정 분할은 메모리를 미리 정해진 크기의 파티션으로 나누는 방식입니다. 가변 분할은 프로세스 크기에 맞게 동적으로 메모리를 나눕니다.

각각 장단점이 있어서 상황에 따라 선택합니다.

다음 코드를 살펴봅시다.

// 고정 분할 방식
public class FixedPartition {
    // 메모리를 고정 크기로 미리 분할
    private int[] partitions = {100, 200, 300, 400};  // KB 단위
    private boolean[] occupied = {false, false, false, false};

    public int allocate(int processSize) {
        for (int i = 0; i < partitions.length; i++) {
            // 프로세스 크기보다 크거나 같은 빈 파티션 찾기
            if (!occupied[i] && partitions[i] >= processSize) {
                occupied[i] = true;
                // 내부 단편화: partitions[i] - processSize 만큼 낭비
                return i;
            }
        }
        return -1;  // 할당 실패
    }
}

// 가변 분할: 프로세스 크기에 딱 맞게 할당
// -> 내부 단편화 없음, 외부 단편화 발생 가능

김개발 씨는 메모리 관리의 두 가지 접근 방식에 대해 더 알고 싶어졌습니다. 박시니어 씨가 화이트보드에 그림을 그리며 설명을 시작했습니다.

"메모리를 나누는 방법은 크게 두 가지야. 미리 나눠놓거나, 그때그때 나누거나." 고정 분할(Fixed Partitioning) 방식을 먼저 살펴봅시다.

비유하자면, 아파트 주차장의 구획과 같습니다. 주차 공간이 미리 흰색 선으로 구분되어 있습니다.

경차든 SUV든 한 칸씩 사용합니다. 경차가 들어가면 공간이 남고, SUV가 들어가면 딱 맞습니다.

고정 분할도 마찬가지입니다. 메모리를 미리 100KB, 200KB, 300KB 등의 파티션으로 나눠놓습니다.

150KB 프로세스가 오면 200KB 파티션에 배치합니다. 50KB가 남지만 다른 프로세스는 이 공간을 사용할 수 없습니다.

고정 분할의 장점은 구현이 간단하다는 것입니다. 미리 나눠놓았으니 복잡한 계산이 필요 없습니다.

단점은 내부 단편화가 발생한다는 것입니다. 파티션 안에서 사용되지 않는 공간이 낭비됩니다.

가변 분할(Variable Partitioning) 방식은 다릅니다. 비유하자면, 텅 빈 공터에 주차하는 것과 같습니다.

구획선이 없어서 차 크기에 딱 맞게 주차합니다. 경차는 경차 크기만큼, SUV는 SUV 크기만큼 공간을 차지합니다.

가변 분할에서는 프로세스가 요청한 크기만큼만 메모리를 할당합니다. 150KB 프로세스가 오면 정확히 150KB만 할당합니다.

따라서 내부 단편화가 없습니다. 하지만 다른 문제가 생깁니다.

프로세스들이 생성되고 종료되면서 메모리 곳곳에 빈 공간이 흩어집니다. 이것이 외부 단편화입니다.

총 빈 공간은 충분한데 연속되지 않아서 새 프로세스를 수용하지 못합니다. 결국 두 방식 모두 단편화 문제를 피할 수 없습니다.

고정 분할은 내부 단편화, 가변 분할은 외부 단편화가 발생합니다. 그렇다면 어떤 방식이 더 나을까요?

일반적으로 가변 분할이 메모리를 더 효율적으로 사용합니다. 내부 단편화처럼 완전히 사용 불가능한 공간이 없기 때문입니다.

외부 단편화는 **압축(Compaction)**으로 해결할 수 있습니다. 흩어진 프로세스들을 한쪽으로 밀어서 빈 공간을 합치는 것입니다.

하지만 압축은 비용이 많이 듭니다. 모든 프로세스를 멈추고 이동시켜야 합니다.

그래서 현대 운영체제는 페이징이라는 더 효율적인 방법을 사용합니다. 박시니어 씨가 정리했습니다.

"고정 분할과 가변 분할은 메모리 관리의 역사야. 이 방식들의 한계가 페이징과 세그멘테이션이 탄생한 배경이지."

실전 팁

💡 - 고정 분할은 실시간 시스템처럼 예측 가능성이 중요한 환경에서 여전히 사용됩니다

  • 가변 분할의 외부 단편화 문제는 페이징으로 해결할 수 있습니다

6. 단편화 (내부/외부)

김개발 씨가 서버 모니터링 대시보드를 보다가 이상한 점을 발견했습니다. "메모리 사용률이 70%인데 왜 새 프로세스가 할당 실패하죠?" 박시니어 씨가 대답했습니다.

"단편화 때문이야."

단편화는 메모리에 빈 공간이 있지만 사용하지 못하는 현상입니다. 내부 단편화는 할당된 메모리 안에서의 낭비이고, 외부 단편화는 할당되지 않은 작은 공간들이 흩어져 있는 현상입니다.

다음 코드를 살펴봅시다.

// 단편화 문제 시각화
public class FragmentationDemo {
    public static void main(String[] args) {
        // === 내부 단편화 예시 ===
        // 파티션 크기: 100KB, 프로세스 크기: 70KB
        int partition = 100;
        int process = 70;
        int internalFrag = partition - process;  // 30KB 낭비

        // === 외부 단편화 예시 ===
        // 메모리 상태: [프로세스A 50KB][빈 공간 30KB][프로세스B 80KB][빈 공간 40KB]
        // 총 빈 공간: 70KB, 하지만 연속된 최대 공간: 40KB
        // 50KB 프로세스 할당 불가! (외부 단편화)

        int[] freeSpaces = {30, 40};  // 빈 공간들
        int totalFree = 70;  // 합계
        int needed = 50;  // 필요한 연속 공간
        // 할당 실패: 연속된 50KB 공간 없음
    }
}

김개발 씨는 운영 중인 서버에서 이상한 현상을 목격했습니다. 메모리 사용률이 70%인데도 새로운 프로세스가 "메모리 부족"으로 실패했습니다.

분명히 30%나 남았는데 왜 사용하지 못하는 걸까요? 박시니어 씨가 설명했습니다.

"그게 바로 단편화 문제야." 단편화는 마치 옷장 정리와 같습니다. 옷장에 빈 공간이 이곳저곳 조금씩 있습니다.

총 빈 공간을 합치면 두꺼운 패딩 점퍼가 들어갈 만합니다. 하지만 연속된 공간이 없어서 점퍼를 넣지 못합니다.

이것이 외부 단편화입니다. 반면 내부 단편화는 다른 상황입니다.

옷장 칸마다 정해진 높이가 있다고 가정합시다. 얇은 티셔츠를 넣어도 한 칸을 다 차지합니다.

칸 안에 빈 공간이 남지만 다른 옷을 넣을 수 없습니다. 이것이 내부 단편화입니다.

컴퓨터 메모리에서 내부 단편화는 고정 분할에서 주로 발생합니다. 100KB 파티션에 70KB 프로세스를 넣으면 30KB가 낭비됩니다.

이 30KB는 파티션 안에 갇혀서 다른 프로세스가 사용할 수 없습니다. 외부 단편화는 가변 분할에서 발생합니다.

프로세스들이 생성되고 종료되면서 메모리 곳곳에 작은 빈 공간들이 생깁니다. 총 빈 공간은 100KB인데, 30KB와 70KB로 떨어져 있다면 50KB 프로세스도 들어갈 수 없습니다.

그렇다면 단편화를 어떻게 해결할까요? 내부 단편화는 가변 분할을 사용하면 해결됩니다.

프로세스 크기에 딱 맞게 메모리를 할당하면 낭비가 없습니다. 외부 단편화는 **압축(Compaction)**으로 해결할 수 있습니다.

메모리에 흩어진 프로세스들을 한쪽으로 밀어서 빈 공간을 하나로 합칩니다. 하지만 모든 프로세스를 멈추고 이동시켜야 해서 비용이 큽니다.

더 좋은 해결책이 있습니다. 바로 페이징입니다.

메모리를 고정 크기의 작은 조각(페이지)으로 나누면, 프로세스를 연속되지 않은 여러 페이지에 분산 배치할 수 있습니다. 외부 단편화가 원천적으로 해결됩니다.

다만 페이징에서도 내부 단편화는 완전히 사라지지 않습니다. 페이지 크기가 4KB일 때, 5KB 프로세스는 2개의 페이지를 사용합니다.

3KB가 낭비됩니다. 하지만 페이지 크기가 작으므로 낭비도 적습니다.

김개발 씨가 고개를 끄덕였습니다. "그래서 서버 메모리가 30% 남았는데도 할당이 안 됐군요.

외부 단편화 때문이었네요." 박시니어 씨가 조언했습니다. "가끔 서버를 재시작하거나, 메모리 정리를 해주는 것이 도움이 될 수 있어.

하지만 근본적으로는 운영체제의 메모리 관리 방식을 이해하고 적절한 설정을 해야 해." 단편화는 메모리 관리에서 피할 수 없는 문제입니다. 하지만 이를 이해하면 시스템의 이상 동작을 진단하고 해결책을 찾을 수 있습니다.

실전 팁

💡 - 메모리 사용률과 할당 가능 용량은 다를 수 있습니다. 단편화를 고려하세요

  • 페이징은 외부 단편화를 해결하지만 내부 단편화는 완전히 없애지 못합니다

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#OperatingSystem#MemoryManagement#VirtualMemory#AddressBinding#Fragmentation#Operating System

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.