Python

파이썬 Garbage Collection 완벽 분석: 개발자가 반드시 알아야 할 모든 것

드리프트2 2025. 3. 19. 22:54

파이썬 Garbage Collection 완벽 분석: 개발자가 반드시 알아야 할 모든 것

 

Garbage Collection, 이제부터 자세히 알아볼까요?

 

컴퓨터 과학에서 Garbage Collection, 줄여서 GC라고 하는데요, 자동 메모리 관리 방식입니다.

프로그램이 쓰는 메모리 공간 중에서 더 이상 필요 없어진 부분을 GC 알고리즘을 써서 운영체제에 다시 돌려주는 것인데요.

Garbage collector는 프로그래머들이 메모리 관리에 쏟는 노력을 줄여주고, 프로그램 에러를 최소화하는 데 도움을 줍니다.

이 기술은 LISP 언어에서 처음 시작됐다고 합니다.

지금은 Smalltalk, Java, C#, Go, D 같은 다양한 언어들이 garbage collector를 지원하고 있습니다.

현대 프로그래밍 언어의 자동 메모리 관리 방식인 GC는 크게 두 가지 중요한 역할을 합니다.

  • 메모리 안에서 더 이상 쓸모 없어진 garbage 자원을 찾아내는 것입니다.
  • 찾아낸 garbage를 깨끗하게 정리해서, 다른 객체들이 메모리를 다시 사용할 수 있도록 해주는 것입니다.

GC 덕분에 개발자들은 복잡하고 귀찮은 리소스 관리에 신경 쓰지 않고, 더 중요한 비즈니스 로직에 집중할 수 있게 됩니다.

하지만 그렇다고 해서 GC를 아예 몰라도 되는 건 아니에요.

GC를 제대로 이해하면 더욱 튼튼하고 효율적인 코드를 짤 수 있거든요.

 

Garbage Collection 알고리즘, 종류별로 알아볼까요?

 

Reference Counting (참조 카운팅):

 

각 객체마다 참조 횟수를 기록해두는 방식인데요.

특정 객체를 참조하는 객체가 사라지면, 그 객체의 참조 횟수를 1씩 줄여나갑니다.

그러다가 참조 횟수가 0이 되면, 그 객체는 더 이상 쓰이지 않는 garbage로 판단하고 메모리에서 회수하는 방식입니다.

대표적인 언어: Python, PHP, Swift 등이 있습니다.

장점: 객체를 바로바로 회수하기 때문에 속도가 빠릅니다.

메모리가 부족해지거나 특정 시점에 도달해야 회수하는 방식이 아니라, 필요 없어지는 즉시 회수합니다.

단점: 순환 참조는 제대로 처리하지 못한다는 단점이 있습니다.

또, 참조 횟수를 계속해서 업데이트하고 관리하는 데 추가적인 부담이 발생할 수 있습니다.

Mark-Sweep (마크-스윕):

 

root 변수에서 시작해서, 연결된 모든 객체를 찾아가면서 '사용 중'이라고 표시(mark)합니다.

그리고 나서 '사용 중' 표시가 안 된 객체들을 garbage로 간주하고 메모리에서 회수(sweep)하는 방식입니다.

대표적인 언어: Golang (삼색 마킹 기법), Python (보조적으로 사용) 등이 있습니다.

장점: Reference Counting 방식의 단점인 순환 참조 문제를 해결할 수 있습니다.

단점: STW(Stop-The-World)라는 과정이 필요합니다.

STW는 garbage collection을 실행하기 위해 프로그램 실행을 잠시 멈추는 것을 말합니다.

Generational Collection (세대별 수집):

 

객체의 수명에 따라 메모리 공간을 여러 세대로 나누어 관리하는 방식입니다.

오래 살아남는 객체는 'old generation(old 영역)'에, 금방 생성되고 사라지는 객체는 'new generation(young 영역)'에 배치합니다.

각 세대별로 garbage collection 알고리즘과 실행 빈도를 다르게 적용합니다.

대표적인 언어: Java, Python (보조적으로 사용) 등이 있습니다.

장점: garbage collection 성능이 뛰어납니다.

단점: 알고리즘이 복잡합니다.

 

파이썬 Garbage Collection은 어떻게 작동할까요?

 

파이썬 공식 문서 설명:

파이썬의 메모리 관리는 구현 방식에 따라 조금씩 다를 수 있습니다.

CPython의 경우, Reference Counting을 사용해서 접근 불가능한 객체를 감지하고, 순환 참조를 처리하기 위한 별도의 메커니즘도 가지고 있습니다.

주기적으로 순환 참조 감지 알고리즘을 실행해서 접근 불가능한 순환 참조를 찾아내고, 관련된 객체들을 삭제합니다.

gc 모듈을 사용하면 프로그래머가 직접 garbage collection을 수행하거나, 디버깅 통계 정보를 얻거나, garbage collector 파라미터를 조정하는 등의 작업을 할 수 있습니다.

Jython이나 PyPy 같은 다른 파이썬 구현체들은 완전 garbage collector와 같이 다른 메커니즘을 사용할 수도 있습니다.

만약 파이썬 코드가 Reference Counting 방식으로 구현된 동작에 의존한다면, 다른 환경으로 옮겨갔을 때 portability 문제가 발생할 수 있습니다.

Reference Counting:

 

파이썬의 기본 garbage collection 메커니즘은 Reference Counting 방식인데요, 1960년에 George E.

Collins가 처음 제안했고, 지금까지도 많은 프로그래밍 언어에서 사용하고 있는 방식입니다.

원리: 모든 객체는 ob_ref라는 필드를 가지고 있는데, 이 필드에 현재 객체를 참조하고 있는 횟수가 기록됩니다.

새로운 참조가 객체를 가리키면 ob_ref 값이 1씩 증가하고, 참조가 끊어지면 1씩 감소합니다.

그러다가 참조 횟수가 0이 되면, 객체는 즉시 garbage로 처리되어 메모리에서 회수되고, 차지하고 있던 메모리 공간이 해제됩니다.

단점: 참조 횟수를 저장하기 위한 추가 공간이 필요하고, 객체끼리 서로 참조하는 "순환 참조" 문제를 해결할 수 없습니다.

예를 들어 아래 코드를 한번 볼까요?

a = {}  # 객체 A의 참조 카운트: 1
b = {}  # 객체 B의 참조 카운트: 1
a['b'] = b  # 객체 B의 참조 카운트 1 증가
b['a'] = a  # 객체 A의 참조 카운트 1 증가
del a  # 객체 A의 참조 -1, 최종 참조 카운트: 1
del b  # 객체 B의 참조 -1, 최종 참조 카운트: 1

 

위 예시 코드에서 del 구문이 실행된 후, 객체 A와 B는 서로 순환 참조하는 상태가 됩니다.

외부에서는 더 이상 참조하지 않지만, 참조 카운트가 0이 되지 않기 때문에 garbage collection이 되지 않고, 메모리 누수가 발생할 수 있습니다.

Mark-Sweep:

 

Mark-Sweep은 tracing GC 기술을 기반으로 구현된 garbage collection 알고리즘인데요, 크게 두 단계로 진행됩니다.

  • Marking 단계: 'active object', 즉 '사용 중인 객체'를 찾아 표시합니다.
  • Sweeping 단계: 'inactive object', 즉 '사용 중이지 않은 객체'로 표시된 객체들을 garbage로 처리해서 메모리에서 회수합니다.

root 객체 (global 변수, call stack, register 등) 부터 시작해서, 객체 그래프를 따라 reachable한 객체들을 'active object'로 마크합니다.

반대로 reachable하지 않은 객체들은 'inactive object'로 마크한 후, sweep 단계에서 정리합니다.

Mark-Sweep 알고리즘은 파이썬에서 보조적인 garbage collection 기술로 사용되는데요, 주로 컨테이너 객체(list, dict, tuple, instance 등)를 처리하는 데 활용됩니다.

왜냐하면 string이나 숫자형 객체는 순환 참조 문제를 일으키지 않기 때문입니다.

파이썬은 doubly linked list를 사용해서 이러한 컨테이너 객체들을 관리합니다.

단점: inactive object를 회수하기 전에 전체 heap 메모리를 쭉 스캔해야 합니다.

active object가 얼마 남지 않았더라도, 모든 객체를 스캔해야 한다는 단점이 있습니다.

Generational Recycling:

 

Generational Recycling은 '공간을 더 쓰고 시간을 절약한다'는 아이디어를 바탕으로 만들어진 방식입니다.

객체가 살아있는 기간에 따라 메모리를 여러 세트로 나누고, 각 세대를 'generation'이라고 부릅니다.

파이썬에서는 young generation (0세대), middle generation (1세대), old generation (2세대) 이렇게 세 개의 세대로 나누어 관리하고, 각 세대는 linked list 형태로 관리됩니다.

객체의 수명이 길어질수록 garbage collection 실행 빈도를 줄여서 효율성을 높입니다.

새로 생성된 객체는 young generation에 할당됩니다.

young generation의 linked list에 객체 수가 특정 threshold를 넘으면, garbage collection이 실행됩니다.

young generation에서 garbage로 판단된 객체는 회수되고, garbage가 아닌 객체는 middle generation으로 옮겨집니다.

middle generation에서도 같은 방식으로 garbage collection이 진행되고, 살아남은 객체는 old generation으로 옮겨집니다.

old generation에 있는 객체는 수명이 가장 긴 객체들입니다.

Generational Recycling은 Mark-Sweep 기술을 기반으로 만들어졌으며, 파이썬에서는 컨테이너 객체를 처리하는 보조적인 garbage collection 기술로 활용됩니다.

메모리 누수, 왜 발생할까요?

일상적인 파이썬 프로그래밍에서 메모리 누수가 흔하게 발생하는 것은 아닙니다.

CPython이 종료될 때 메모리를 제대로 해제하지 못하는 몇 가지 상황이 있습니다.

  • global namespace나 파이썬 모듈에서 참조되는 객체들은 가끔 해제되지 않을 수 있습니다. 특히 순환 참조가 있을 때 이런 문제가 발생할 수 있습니다. C 라이브러리에서 할당된 메모리 중 일부도 해제되지 않을 수 있습니다.
  • 파이썬은 종료될 때 메모리를 정리하고 모든 객체를 삭제하려고 시도합니다.

만약 특정 시점에 파이썬이 특정 내용을 강제로 삭제하도록 하고 싶다면, atexit 모듈을 사용해서 함수를 실행하도록 설정할 수 있습니다.

코드 예시:

# (CPython에서는 문제없이 동작하지만) 일부 파이썬 구현체에서는 아래 코드가 file descriptor를 소진시킬 수 있습니다.
for file in very_long_list_of_files:
    f = open(file)
    c = f.read(1)

 

개선된 해결 방법:

# 파일을 명시적으로 닫거나 with 구문을 사용하는 것이 좋습니다. with 구문은 메모리 관리 방식에 상관없이 효과적입니다.
for file in very_long_list_of_files:
    with open(file) as f:
        c = f.read(1)