2025-08-30 19:47
-
가비지 컬렉션]은 프로그래머가 직접 하지 않아도 되는, 사용하지 않는 메모리를 자동으로 정리해 주는 똑똑한 청소부입니다.
-
GC는 ‘Root’에서 시작해 살아있는 객체를 표시(Mark)하고, 표시되지 않은 쓰레기 객체들을 치우는(Sweep) 방식으로 동작합니다.
-
최신 GC는 객체를 수명에 따라 다른 영역(Young/Old)에 배치하여, 더 빠르고 효율적으로 메모리를 관리하는 세대별 방식을 주로 사용합니다.
메모리 관리의 숨은 영웅 가비지 컬렉션 완벽 핸드북
우리가 코드를 작성하고 프로그램을 실행할 때, 보이지 않는 곳에서 시스템의 안정성과 성능을 묵묵히 책임지는 기술이 있습니다. 바로 ‘가비지 컬렉션(Garbage Collection, GC)‘입니다. 개발자가 일일이 메모리를 할당하고 해제하는 번거로움에서 벗어나 비즈니스 로직에만 집중할 수 있도록 해주는 핵심적인 기능이죠.
마치 우리가 어지럽힌 방을 알아서 청소해 주는 로봇 청소기처럼, 가비지 컬렉션은 애플리케이션이 사용한 메모리 중에서 더 이상 필요 없는 영역, 즉 ‘쓰레기(Garbage)‘를 찾아내 자동으로 해제합니다. 이 핸드북에서는 가비지 컬렉션이 왜 만들어졌는지부터 시작해, 어떤 원리로 동작하는지, 그리고 더 나아가 현대적인 가비지 컬렉션 알고리즘까지 깊이 있게 탐험해 보겠습니다.
1. 가비지 컬렉션은 왜 만들어졌을까? (탄생 배경)
가비지 컬렉션의 필요성을 이해하려면, 그 이전 시대의 메모리 관리 방식을 먼저 알아야 합니다. C나 C++ 같은 언어에서는 개발자가 직접 메모리를 관리해야 했습니다.
-
수동 메모리 관리의 시대:
-
할당 (Allocation):
malloc()
함수를 사용해 필요한 만큼 메모리 공간을 운영체제에 요청합니다. -
해제 (De-allocation): 사용이 끝난 메모리는
free()
함수를 호출해 반드시 직접 반납해야 합니다.
-
이 방식은 개발자에게 메모리 통제권을 준다는 장점이 있지만, 치명적인 단점들을 안고 있었습니다. 마치 도서관에서 책을 빌린 뒤, 다 읽고 나서 직접 제자리에 꽂아둬야 하는 것과 같습니다. 만약 깜빡하고 반납하지 않거나, 엉뚱한 곳에 꽂아두면 어떻게 될까요?
-
메모리 누수 (Memory Leak): 사용이 끝난 메모리를 해제하지 않아 메모리가 계속 쌓이는 문제입니다. 프로그램이 오래 실행될수록 가용 메모리가 줄어들어 결국 시스템 전체가 느려지거나 멈출 수 있습니다. (도서관에서 책을 빌리고 반납하지 않는 상황)
-
댕글링 포인터 (Dangling Pointer): 이미 해제된 메모리 주소를 계속 참조하고 있는 경우입니다. 해당 주소에 새로운 데이터가 할당되면, 의도치 않은 값을 읽거나 써서 프로그램이 비정상적으로 종료될 수 있습니다. (반납한 책이 있던 자리를 계속 찾아가는 상황)
-
이중 해제 (Double Free): 이미 해제한 메모리를 또다시 해제하려고 시도하는 문제입니다. 이 또한 심각한 오류를 유발합니다. (반납한 책을 또 반납하려는 상황)
이러한 문제들은 디버깅하기 매우 까다롭고, 개발자의 생산성을 크게 떨어뜨렸습니다. 프로그래머들은 애플리케이션의 핵심 기능 개발보다 메모리 관리 오류를 잡는 데 더 많은 시간을 쏟아야 했습니다.
이런 고통을 해결하기 위해, 1959년 존 매카시(John McCarthy)가 Lisp 언어를 위해 가비지 컬렉션을 고안했습니다. “개발자는 필요한 객체를 생성하기만 해라. 더 이상 쓰이지 않는 객체는 시스템이 알아서 치워주겠다”는 혁신적인 아이디어였습니다. 이로써 개발자는 메모리 관리의 부담을 덜고, 더 안전하고 빠르게 애플리케이션을 개발할 수 있는 시대가 열렸습니다.
2. 가비지 컬렉션의 핵심 원리
가비지 컬렉터는 어떻게 ‘쓰레기’를 알아볼까요? 핵심은 ‘도달 가능성(Reachability)’ 이라는 개념에 있습니다.
객체들을 하나의 거대한 마을이라고 상상해 봅시다. 이 마을에는 외부로 통하는 유일한 길이 몇 개 있는데, 이 길의 시작점을 ‘루트(Root)’ 라고 부릅니다.
-
루트 셋 (Root Set): GC가 탐색을 시작하는 기준점입니다. 여기에 해당하는 것들은 다음과 같습니다.
-
실행 중인 메서드의 지역 변수 및 파라미터
-
정적(Static) 변수
-
JNI (Java Native Interface) 참조 등
-
가비지 컬렉터는 이 ‘루트’에서부터 출발하여, 참조(Reference)라는 길을 따라 연결된 모든 객체들을 방문합니다. 이렇게 루트에서부터 길을 따라 방문할 수 있는 객체는 ‘살아있는 객체(Live Object)’ 로 간주합니다. 반대로, 어떤 길로도 도달할 수 없는 객체는 ‘쓰레기(Garbage)’ 이며, 수거 대상이 됩니다.
이 과정을 조금 더 구체적인 단계로 나누면 보통 다음과 같습니다.
-
표시 (Mark): 루트에서부터 시작하여, 연결된 모든 ‘살아있는’ 객체들을 찾아내어 “이 객체는 사용 중”이라고 표시합니다.
-
청소 (Sweep/Compact/Copy): 표시되지 않은 객체들, 즉 쓰레기를 메모리에서 제거합니다.
이 ‘청소’ 단계에서 어떤 방식을 사용하느냐에 따라 GC 알고리즘의 종류가 나뉩니다.
3. 주요 가비지 컬렉션 알고리즘
가비지 컬렉션은 수십 년간 발전해오면서 다양한 알고리즘을 낳았습니다. 각각의 장단점을 이해하면, 우리가 사용하는 기술 스택의 메모리 관리 방식을 더 깊이 이해할 수 있습니다.
1) Mark-and-Sweep 알고리즘
가장 기본적인 GC 알고리즘입니다. 이름 그대로 ‘표시하고 쓸어버리는’ 두 단계로 동작합니다.
-
Mark (표시) 단계: 루트 셋에서 시작해 참조를 따라가며 모든 살아있는 객체를 표시합니다.
-
Sweep (청소) 단계: 전체 메모리(Heap)를 처음부터 끝까지 훑으면서, 표시되지 않은 객체(쓰레기)를 찾아 메모리에서 해제합니다.
-
장점: 구현이 비교적 간단하고, 순환 참조(두 객체가 서로를 참조하여 쓰레기가 되었지만, 스스로는 참조가 남아있는 상태) 문제를 해결할 수 있습니다.
-
단점:
-
Stop-the-World: GC가 실행되는 동안 애플리케이션의 모든 작업이 멈춥니다. 이 정지 시간이 길어지면 서비스 응답성에 치명적일 수 있습니다.
-
메모리 파편화 (Fragmentation): 메모리 중간중간에 쓰레기가 빠지면서 빈 공간이 생깁니다. 이 빈 공간들이 작게 쪼개져 있으면, 총 가용 메모리는 충분해도 큰 객체를 할당하지 못하는 문제가 발생할 수 있습니다.
-
2) Mark-and-Compact 알고리즘
Mark-and-Sweep의 파편화 문제를 해결하기 위해 등장했습니다.
-
Mark (표시) 단계: Mark-and-Sweep과 동일하게 살아있는 객체를 표시합니다.
-
Compact (압축) 단계: 표시된 객체들을 메모리의 한쪽으로 전부 이동시켜 차곡차곡 쌓습니다. 이로써 파편화된 빈 공간들은 하나의 큰 덩어리가 됩니다.
-
장점: 파편화 문제를 해결하여 메모리 할당 속도가 빨라지고 효율성이 높아집니다.
-
단점: 객체를 이동시키는 데 추가적인 비용(Overhead)이 발생하여, Sweep 방식보다 GC 실행 시간이 더 길어질 수 있습니다.
3) Copying 알고리즘
메모리 영역을 둘로 나누어 활용하는 독특한 방식입니다. 힙(Heap) 영역을 ‘From’ 공간과 ‘To’ 공간으로 나눕니다.
-
애플리케이션은 ‘From’ 공간에만 새로운 객체를 할당합니다.
-
GC가 실행되면, ‘From’ 공간에 있는 살아있는 객체들만 ‘To’ 공간으로 복사합니다.
-
복사가 끝나면, ‘From’ 공간에 남아있는 모든 객체는 쓰레기이므로 공간 전체를 깨끗하게 비웁니다.
-
마지막으로 ‘From’과 ‘To’의 역할을 서로 바꿉니다.
-
장점:
-
매우 빠른 GC 속도를 보입니다. 살아있는 객체만 복사하고 나머지 공간은 통째로 지우기 때문입니다.
-
파편화가 전혀 발생하지 않습니다.
-
-
단점: 전체 힙 공간의 절반밖에 사용하지 못해 메모리 사용 효율이 떨어집니다.
4) 세대별 가비지 컬렉션 (Generational GC)
현대의 대부분의 GC는 이 ‘세대별’ 가설을 기반으로 동작합니다. 이는 Copying 알고리즘의 장점과 다른 알고리즘의 장점을 결합한 매우 효율적인 방식입니다.
세대별 가설 (Weak Generational Hypothesis):
-
대부분의 객체는 생성된 직후 쓰레기가 된다. (수명이 매우 짧다)
-
오래 살아남은 객체는 앞으로도 계속 살아남을 가능성이 높다.
이 가설에 따라, 힙 영역을 객체의 ‘나이’에 따라 두 세대(Generation)로 나눕니다.
-
Young Generation (젊은 세대):
-
새로 생성된 객체들이 위치하는 영역입니다.
-
이 영역은 다시 Eden, Survivor 0 (S0), Survivor 1 (S1) 세 구역으로 나뉩니다.
-
대부분의 객체가 여기서 생성되고 사라지므로, GC가 매우 빈번하게 발생합니다. 이를 Minor GC라고 부릅니다.
-
Minor GC는 Copying 알고리즘을 사용하여 매우 빠르게 동작합니다.
-
-
Old Generation (늙은 세대):
-
Young Generation에서 여러 번의 GC 동안 살아남은 객체들이 이동(Promotion)되는 영역입니다.
-
이곳의 객체들은 오랫동안 살아남을 가능성이 높다고 간주되므로, GC가 덜 빈번하게 발생합니다.
-
이 영역에서 발생하는 GC를 Major GC 또는 Full GC라고 부르며, Mark-and-Sweep이나 Mark-and-Compact 방식을 사용합니다.
-
세대별 GC 동작 과정:
-
새로운 객체는 Young Generation의 Eden 영역에 할당됩니다.
-
Eden 영역이 꽉 차면 Minor GC가 발생합니다.
-
Eden의 살아있는 객체는 S0으로, 기존 S0의 살아있는 객체는 S1으로 복사됩니다. (Copying 방식)
-
이 과정을 반복하며 특정 횟수(Age) 이상 살아남은 객체는 Old Generation으로 이동합니다.
-
Old Generation 영역이 꽉 차면 Major GC가 발생하여 전체 힙을 대상으로 쓰레기를 정리합니다.
이 방식은 수명이 짧은 객체들은 Young Generation에서 빠르고 효율적으로 처리하고, 수명이 긴 객체들은 Old Generation에서 덜 자주 검사함으로써 전체적인 GC 효율을 극대화합니다.
알고리즘 종류 | 동작 방식 | 장점 | 단점 |
---|---|---|---|
Mark-and-Sweep | 살아있는 객체를 표시하고, 나머지를 제거 | 순환 참조 해결, 기본적 | Stop-the-World, 메모리 파편화 |
Mark-and-Compact | 살아있는 객체를 표시하고, 한쪽으로 압축 | 파편화 해결, 메모리 효율 향상 | 객체 이동 비용 발생 |
Copying | 힙을 둘로 나눠 살아있는 객체만 복사 | 매우 빠름, 파편화 없음 | 메모리 사용 효율이 절반으로 감소 |
Generational | 객체 수명에 따라 영역을 나누어 관리 | 전체적인 GC 성능 및 효율성 극대화 | 구조가 상대적으로 복잡함 |
4. 가비지 컬렉션과 개발자
가비지 컬렉션은 자동화된 기술이지만, 개발자가 그 동작 방식을 이해하면 더 나은 성능의 애플리케이션을 만들 수 있습니다.
-
Stop-the-World 이해하기: 어떤 GC 알고리즘이든 정도의 차이는 있지만 ‘Stop-the-World’는 발생합니다. 특히 Full GC는 정지 시간이 길어 실시간 서비스에 영향을 줄 수 있습니다. 따라서 Full GC가 너무 자주 발생하지 않도록 코드를 작성하는 것이 중요합니다. 예를 들어, 불필요한 객체, 특히 수명이 긴 객체를 과도하게 생성하는 것을 피해야 합니다.
-
GC 튜닝: Java (JVM) 같은 환경에서는 다양한 GC 옵션을 제공합니다. 애플리케이션의 특성(예: 응답 시간이 중요한 웹 서버, 처리량이 중요한 배치 작업)에 따라 적절한 GC 알고리즘(G1GC, ZGC, Shenandoah GC 등)을 선택하고 힙 크기 등의 파라미터를 조정하여 성능을 최적화할 수 있습니다.
-
객체 참조 관리: 더 이상 사용하지 않는 객체에 대한 참조를 명시적으로
null
로 만들어주면, GC가 해당 객체를 더 빨리 쓰레기로 식별하는 데 도움이 될 수 있습니다. (물론, 대부분의 경우에는 지역 변수의 스코프가 끝나면 자동으로 참조가 해제되므로 과도하게 사용할 필요는 없습니다.)
결론
가비지 컬렉션은 현대 프로그래밍 언어의 생산성과 안정성을 뒷받침하는 핵심 기술입니다. 수동 메모리 관리의 위험과 번거로움에서 개발자를 해방시켜 주었고, 복잡한 애플리케이션을 더 쉽게 만들 수 있도록 했습니다.
단순한 Mark-and-Sweep에서 시작하여, 파편화를 해결한 Mark-and-Compact, 속도를 높인 Copying, 그리고 이 모든 것의 장점을 집대성한 세대별 GC에 이르기까지, 가비지 컬렉션은 ‘어떻게 하면 애플리케이션의 중단을 최소화하면서 메모리를 효율적으로 관리할 것인가’라는 목표를 향해 끊임없이 진화해 왔습니다.
이제 우리는 코드를 작성할 때마다, 보이지 않는 곳에서 묵묵히 메모리를 청소하며 우리의 애플리케이션을 지탱해주는 이 ‘숨은 영웅’의 존재를 기억할 수 있을 것입니다.