본문 바로가기
카테고리 없음

가비지 컬렉션(GC)과 자료구조의 관계

by 코드 아카이브 2026. 2. 3.

현대 프로그래밍 언어의 대다수(Java, Python, JavaScript, C# 등)는 개발자의 생산성 향상을 위해 메모리 관리를 자동화하는 가비지 컬렉션(Garbage Collection, 이하 GC)을 채택하고 있습니다. 그러나 GC가 존재한다고 해서 개발자가 메모리 관리의 책임으로부터 완전히 자유로워지는 것은 아닙니다. 특히 대규모 트래픽을 처리하거나 제한된 리소스 환경에서 애플리케이션을 운용할 때, 잘못 선택된 자료구조는 GC의 빈번한 호출(Stop-the-world)을 유발하여 치명적인 성능 저하를 일으킬 수 있습니다.

많은 개발자가 알고리즘의 시간 복잡도(Big-O)에는 민감하게 반응하지만, 자료구조의 구조적 특성이 메모리 해제 과정에 미치는 영향은 간과하는 경향이 있습니다. 본 글에서는 가비지 컬렉션의 핵심 작동 원리를 분석하고, 특정 자료구조가 GC의 성능에 어떤 영향을 미치는지, 그리고 이를 최적화하기 위해 개발자가 고려해야 할 기술적 전략은 무엇인지 심도 있게 다루겠습니다.

1. 가비지 컬렉션(GC)의 핵심 메커니즘 이해

자료구조와의 관계를 파악하기 위해서는 먼저 GC가 '무엇을 기준으로' 메모리를 해제하는지 이해해야 합니다. GC의 가장 기본적인 전제는 '도달 가능성(Reachability)'입니다. 힙(Heap) 영역에 할당된 객체 중, 유효한 참조(Reference)가 없는 객체는 가비지(Garbage)로 간주되어 수거 대상이 됩니다.

1-1. Root Set과 참조 체인

GC는 'Root Set'이라 불리는 시작점(스택 변수, 전역 변수, 레지스터 등)에서 출발하여 참조된 모든 객체를 탐색합니다. 이 과정에서 도달할 수 있는 객체는 'Live' 상태로 마킹(Mark)하고, 도달할 수 없는 객체는 메모리에서 해제(Sweep)합니다. 즉, 자료구조가 객체를 어떻게 참조하고 유지하느냐에 따라 GC의 탐색 비용이 결정됩니다.

1-2. Mark-and-Sweep 알고리즘의 비용

대표적인 GC 알고리즘인 Mark-and-Sweep은 객체 그래프를 순회해야 합니다. 만약 자료구조가 불필요하게 복잡하거나, 해제되어야 할 객체의 참조를 끊지 않고 유지하고 있다면, Mark 단계에서의 수행 시간이 길어지며 애플리케이션의 일시 정지 시간이 증가하게 됩니다.

2. 자료구조의 선택이 GC 성능에 미치는 영향

적절한 자료구조의 선택은 단순히 데이터의 삽입/삭제 속도뿐만 아니라, 메모리 효율성과 GC 부하 감소에도 직접적인 영향을 줍니다. 자료구조의 내부 구현 방식에 따라 GC가 겪는 부하는 달라집니다.

2-1. 배열(Array) vs 연결 리스트(Linked List)

메모리 관리 관점에서 배열과 연결 리스트는 큰 차이를 보입니다.

  • 배열(Array): 연속된 메모리 공간을 사용합니다. 이는 참조의 지역성(Locality of Reference)이 높아 CPU 캐시 적중률을 높일 뿐만 아니라, GC가 메모리 블록을 스캔할 때도 매우 효율적입니다. 포인터(참조)를 따라가는 오버헤드가 적습니다.
  • 연결 리스트(Linked List): 각 노드가 메모리 상에 흩어져 있으며, 서로를 참조로 연결합니다. GC는 이 연결 고리를 하나하나 따라가며 탐색해야 하므로, 노드가 많아질수록 Mark 단계의 비용이 급격히 증가합니다. 또한, 각 노드 객체마다 별도의 헤더 정보가 필요하므로 메모리 파편화(Fragmentation)를 유발할 가능성이 높습니다.

2-2. 해시 테이블(Hash Table)과 메모리 누수

HashMap이나 Dictionary와 같은 해시 기반 자료구조는 의도치 않은 메모리 누수(Memory Leak)의 주범이 되기도 합니다. 키(Key)와 값(Value) 쌍을 저장한 후, 더 이상 필요하지 않음에도 명시적으로 삭제하지 않으면 GC는 이를 '도달 가능한 객체'로 판단하여 수거하지 못합니다. 이는 'Loitering Objects(배회하는 객체들)' 문제를 야기하며, 결국 OOM(Out of Memory) 오류로 이어질 수 있습니다.

3. GC 최적화를 위한 기술적 해결책 및 코드 패턴

GC의 부담을 줄이고 효율적인 메모리 관리를 수행하기 위해서는 자료구조 사용 시 다음과 같은 코딩 패턴을 적용해야 합니다.

3-1. 불필요한 참조 제거 (Nulling out references)

스택(Stack)과 같이 자체적으로 메모리를 관리하는 자료구조를 구현하거나 사용할 때는, 더 이상 쓰이지 않는 객체에 대한 참조를 명시적으로 끊어야 합니다. 아래는 Java를 예시로 한 스택 구현의 올바른 메모리 해제 패턴입니다.


public Object pop() {
    if (size == 0) {
        throw new EmptyStackException();
    }
    
    // 배열에서 객체를 꺼냄
    Object result = elements[--size];
    
    // [중요] 다 쓴 참조를 null로 초기화하여 GC가 수거하도록 유도
    elements[size] = null; 
    
    return result;
}

위 코드에서 elements[size] = null; 부분이 없다면, 스택에서 해당 요소는 논리적으로는 삭제되었으나 물리적으로는 여전히 배열이 참조하고 있게 됩니다. 이를 'Obsolete Reference(만기 참조)'라고 하며, GC의 수거를 방해하는 주요 원인입니다.

3-2. WeakReference(약한 참조)의 활용

캐시(Cache)와 같이 '있으면 쓰고, 없으면 다시 만들면 되는' 데이터를 저장하는 자료구조에는 WeakReference를 사용하는 것이 유리합니다. 강한 참조(Strong Reference)와 달리, 약한 참조로 연결된 객체는 GC 실행 시 무조건 수거 대상이 되므로, 캐시 데이터가 메모리를 과도하게 점유하는 것을 방지할 수 있습니다. Java의 WeakHashMap이 대표적인 예입니다.

[핵심 요약]
1. GC의 작동 원리: GC는 Root Set에서 도달 가능한 객체(Reachability)를 기준으로 메모리를 관리하며, 참조 관계가 복잡할수록 탐색 비용이 증가합니다.
2. 자료구조와 성능: 연결 리스트보다 배열 기반 자료구조가 메모리 지역성과 스캔 효율성 면에서 GC 친화적입니다.
3. 개발자의 역할: 사용이 끝난 객체의 참조를 null 처리하거나 WeakReference를 활용하여 GC가 원활하게 작동하도록 돕는 것이 필수적입니다.