[로봇활용_12주차] C# 가비지 컬렉터(Garbage Collector)

최윤호·2025년 10월 28일
post-thumbnail

C#의 보이지 않는 청소부

C#에서 보이지 않는 곳에서 묵묵히 일하는 친구가 있습니다.
바로 가비지 컬렉터(Garbage Collector)입니다.
코드를 작성하다 보면 수많은 객체가 메모리에 생성되었다가 사라지곤 합니다.
마치 우리가 파티를 즐기고 나면 뒷정리가 필요한 것처럼,
프로그램이 사용한 메모리도 누군가는 깔끔하게 정리해 주어야 하죠.
C#에서는 가비지 컬렉터가 메모리 청소를 자동으로 담당해 준답니다.
이번 글에서는 가비지 컬렉터가 무엇인지, 어떤 원리로 동작하는지 살펴보겠습니다!

1)가비지 컬렉터가 필요한 이유?

C언어나 C++ 같은 언어에서는 개발자가 직접 메모리를 할당(malloc, new)하고,
사용이 끝나면 반드시 해제(free, delete)해 주어야 했습니다.
만약 깜빡하고 메모리 해제를 잊어버리면 어떻게 될까요?
바로 메모리 누수(Memory Leak)가 발생합니다!
C#에서는 가비지 컬렉터가 자동으로 메모리를 관리해 주기 때문에
개발자는 이런 부담을 덜고 비즈니스 로직에 더 집중할 수 있습니다.

메모리 누수(Memory Leak)란?
더 이상 사용하지 않는 메모리가 해제되지 않고 계속 쌓여서
프로그램이 사용할 수 있는 메모리가 부족해지는 현상입니다.
마치 방 청소를 전혀 하지 않아서 발 디딜 틈도 없게 되는 것과 같아요.

2)가비지 컬렉션과의 차이

가비지 컬렉션은 '가비지 컬렉터'가 메모리를 해제하는 행위를 의미합니다.
쉽게 말하자면 가비지 컬렉터는 '청소부', 가비지 컬렉션은 '청소하는 행동'입니다.

3)'쓰레기'를 찾는 과정

먼저 무엇이 '쓰레기'이고 무엇이 '사용 중인 물건'인지 구분해야 합니다.
가비지 컬렉터는 아주 똑똑한 방법으로 이를 판단하는데요,
바로 '도달 가능성(Reachability)'이라는 개념을 사용합니다.

  • 예를 들어볼까요? 여러분이 손에 실타래의 한쪽 끝을 쥐고 있다고 상상해 보세요.
    손에서부터 직접, 또는 다른 실을 통해 연결된 모든 실은 '사용 중인 실'입니다.
    하지만 중간에 끊어져서 연결되지 않은 실 조각들은 '쓰레기'가 되는 거죠.
  • 루트(Roots): 여러분의 '손'에 해당하는 부분입니다.
    프로그램이 직접 접근할 수 있는 참조 지점들이죠. (예: 전역 변수, 정적 변수 등)
  • 객체 그래프(Object Graph): 실들이 서로 얽혀있는 모습처럼,
    객체들이 서로를 참조하며 연결된 관계를 말합니다.

가비지 컬렉터는 이 '루트'에서부터 시작해서, 참조를 따라 객체 그래프를 탐색합니다.
그리고 루트로부터 도달할 수 있는 모든 객체에 "사용 중!"이라는 표시(Mark)를 남깁니다.
탐색이 끝난 후, "사용 중!" 표시가 없는 객체들은 루트로부터 도달할 수 없는,
즉 더 이상 프로그램에서 사용하지 않는 '쓰레기(Garbage)'로 간주하고
메모리에서 수거(Sweep)해 갑니다. 이것이 'Mark and Sweep' 알고리즘입니다.

[코드]

public class User
{
    private string _name;
    public User(string name)
    {
        _name = name;
        Console.WriteLine($"{_name} 객체가 생성되었습니다.");
    }

    // 소멸자: 가비지 컬렉터에 의해 객체가 수거될 때 호출됩니다. (확인용)
    ~User()
    {
        Console.WriteLine($"{_name} 객체가 수거되었습니다.");
    }
}

class Program
{
    static void Main()
    {
        CreateUsers();

        // CreateUsers 메서드가 종료되면, 그 안의 지역 변수(user1, user2)는
        // 더 이상 '루트'에서 도달할 수 없게 됩니다.
        // 이때 가비지 컬렉터가 동작할 조건이 되면, 해당 객체들은 '쓰레기'로 간주됩니다.

        Console.WriteLine("가비지 컬렉터(GC)를 강제로 호출해봅니다!");
        GC.Collect(); // 학습 목적으로만 사용, 실제 코드에서는 거의 사용하지 않아요!
        GC.WaitForPendingFinalizers();

        Console.WriteLine("프로그램 종료");
    }

    static void CreateUsers()
    {
        User user1 = new User("Alice");
        User user2 = new User("Bob");

        // user1이 참조하던 "Alice" 객체는 더 이상 아무도 참조하지 않게 됩니다.
        // 즉, '쓰레기'가 될 후보가 됩니다.
        user1 = user2;

        Console.WriteLine("CreateUsers 메서드 종료");
    }
}

[실행 결과]

Alice 객체가 생성되었습니다.
Bob 객체가 생성되었습니다.
CreateUsers 메서드 종료
가비지 컬렉터(GC)를 강제로 호출해봅니다!
Alice 객체가 수거되었습니다.
Bob 객체가 수거되었습니다.
프로그램 종료

4)메모리 단편화

메모리 단편화를 '도서관 책장'에 비유해서 설명해 보겠습니다.

  • 책장: 컴퓨터의 메모리(RAM)
  • 책: 프로그램이 사용하는 데이터

프로그램이 시작되면, 필요한 데이터(책)들을 책장에 차곡차곡 순서대로 꽂습니다.

[책A] [책B] [책C] [책D] [...빈 공간...]

그런데 프로그램이 실행되다 보면, 더 이상 필요 없는 데이터(책)들이 생기겠죠?
예를 들어, 책B책D를 빼냈다고 해봅시다. 그러면 책장은 이렇게 변합니다.

[책A] [ 빈 공간 ] [책C] [ 빈 공간 ] [...빈 공간...]

바로 이 상태가 메모리 단편화(Memory Fragmentation)입니다.
책장 중간중간에 구멍(빈 공간)이 숭숭 뚫린 상태죠.

문제점: 만약 책B책D를 합친 것보다 더 두꺼운 책E를 꽂으려고 하면 어떻게 될까요?
책장에 남아있는 빈 공간은 충분하지만 낭비되는 공간 때문에 책E를 꽂을 수가 없습니다.

5)메모리 압축

가비지 컬렉터는 메모리 단편화를 해결하기 위해 메모리 압축 작업을 수행합니다.
책장 정리를 하는 사서를 떠올려보세요. 중간에 비어 있는 책장을 보면 어떻게 할까요?
남아있는 책들을 한쪽으로 쭈욱 밀어서 정리하겠죠? 가비지 컬렉터도 똑같이 행동합니다.

  • 표시(Mark): 아직 읽고 있는 중요한 책(책A, 책C)들을 전부 표시합니다.
  • 수거(Sweep): 표시되지 않은 책(책B, 책D가 있던 자리)은 '쓰레기'로 간주하고,
    공간을 비웁니다. (이 과정에서 단편화가 발생합니다.)
  • 압축(Compact): 살아남은 책(책A, 책C)들을 책장 한쪽 끝으로 차곡차곡 모아줍니다.
    • 정리 전: [책A] [ 빈 공간 ] [책C] [...빈 공간...]
    • 정리 후: [책A] [책C] [ 하나의 연속된 빈 공간 ]

결과적으로, 흩어져 있던 작은 빈 공간들이 '하나의 연속된 빈 공간'으로 합쳐졌습니다!
이제 아까 꽂지 못했던 두꺼운 책E도 이 큰 공간에 쉽게 꽂을 수 있게 되었죠.

6)그렇다고 만능은 아니에요!

가비지 컬렉터 덕분에 메모리 관리가 정말 편해졌지만, 몇 가지 주의할 점이 있습니다.

  • 성능 저하: 가비지 컬렉터가 동작하는 순간에는 프로그램의
    모든 스레드가 잠시 멈춥니다. (이를 'Stop-the-World'라고 불러요)
    가비지 컬렉터가 너무 자주, 또는 너무 오래 실행되면
    프로그램이 순간적으로 멈칫하는 현상이 발생할 수 있습니다.
  • 관리되지 않는 리소스(Unmanaged Resources): 가비지 컬렉터는
    .NET이 관리하는 메모리(Managed Heap)만 청소합니다. 파일 핸들, DB 연결,
    네트워크 소켓 등 .NET 외부의 자원은 가비지 컬렉터가 직접 해제해 주지는 못해요.

이러한 '관리되지 않는 리소스'IDisposable인터페이스를 구현하고
using구문을 사용해서 명시적으로 해제해 주는 것이 좋습니다.

// StreamReader는 파일 핸들(Unmanaged Resource)을 사용합니다.
// using 블록이 끝나면 .Dispose() 메서드가 자동으로 호출되어 리소스를 안전하게 해제합니다.
using (StreamReader reader = new StreamReader("myFile.txt"))
{
    string line = reader.ReadLine();
    Console.WriteLine(line);
}

종료자는 가비지 컬렉터가 원할 때 호출되므로, 언제 실행될지 예측할 수 없습니다.
using구문이나 개발자가 직접 .Dispose()를 호출하는 것은
코드상에서 리소스가 해제되는 시점을 명확히 알 수 있습니다.
그래서 종료자는 개발자가 .Dispose()호출을 누락했거나 실패하는 경우를
대비한 '최후의 안전장치(Fallback)' 용도로만 사용해야 합니다.

7)가비지 컬렉터 요약

  • 더 이상 사용하지 않는 객체를 찾아 자동으로 메모리를 해제해 주는 역할을 합니다.
  • '도달 가능성'을 기반으로 연결되지 않은 객체는 '쓰레기'로 판단합니다.
  • 메모리 중간중간에 생기는 메모리 단편화를 해결하여 메모리 효율을 높입니다.
  • 성능 저하관리되지 않는 리소스에 대한 이해는 필요합니다.
profile
🚀 미래의 엔지니어를 꿈꾸는 훈련생의 기록 📝

0개의 댓글