가비지 컬렉터(garbage collector)는 메모리 관리 방법 중 하나로, 사용자가 동적으로 할당하는 영역인 힙 메모리 영역 중 쓰이지 않는 영역을 자동으로 찾아내 해제하는 기능이다. 프로그래밍 초기에는 메모리 할당과 해제를 개발자가 직접 관리해야 했는데 이는 매우 번거롭고 오류가 발생하기 쉬운 작업이었다. 잘못된 메모리 해제나 중복 해제는 프로그램의 비정상 종료를 유발하고 메모리 누수(memory leak)같은 문제를 일으켰다. 이러한 문제를 해결하기 위해, 런타임에서 필요 없는 객체들을 자동으로 감지하고 해제하는 가비지 컬렉터가 도입되었다. 예를들어, C에서는 malloc, calloc, free 같은 명령어로 메모리를 직접 관리하지만, JAVA나 Python 같은 언어는 가비지 컬렉터가 자동으로 메모리를 관리해준다. 이번에는 Python에서 가비지 컬렉터의 동작에 대해 알아보자.
파이썬의 함수 호출방식
메모리 관리에 앞서 파이썬에서의 함수 호출방식을 먼저 알아보자. 함수 호출방식에는 크게 값에 의한 호출, 참조에 의한 호출이 있다.
call by value (값에 의한 호출)
•
함수에 인수를 전달할 때 실제 값의 복사본이 전달되는 방식
•
함수 내에서 매개변수 값을 변경해도 원본 변수에 영향을 주지 않음
call by reference (참조에 의한 호출)
•
함수에 인수를 전달할 때 변수의 메모리 주소를 전달하는 방식
•
변수가 어딜 참조 하고 있는지 메모리 주소를 받기 때문에 함수 내에서 원본 변수를 직접 변경할 수 있음
call by object reference (객체 참조에 의한 호출)
파이썬은 위의 방식이 아닌 객체 참조에 의한 호출을 사용한다. 파이썬에서는 모든 것이 객체이며, 변수는 객체를 가리키는 참조자(reference) 역할을 한다. 객체는 불변과 가변으로 나뉘어 불변 객체는 call by value 처럼 동작하고 가변 객체는 call by reference 처럼 동작한다.
•
불변 객체 : 정수, 문자열, 튜플 등 변경 할 수 없는 객체
•
가변 객체: 리스트, 딕셔너리, 집합 등 변경 가능한 객체
def modify_list(lst):
lst.append(4)
print(f"함수 내 lst: {lst}")
my_list = [1, 2, 3]
modify_list(my_list)
print(f"함수 밖 my_list: {my_list}")
Python
복사
함수 내 lst: [1, 2, 3, 4]
리스트는 가변 객체이기 때문에 lst와 my_list는 변수명은 다르지만 동일한 객체를 참조하여 같은 값이 출력된다. 파이썬에서는 객체마다 참조 현황인 참조 카운트를 관리한다. (현재 예시의 리스트 객체는 2개의 변수에 의해 참조되고 있다.) 이 참조 카운트를 토대로 가비지 컬렉터가 동작하게 된다.
파이썬의 가비지 컬렉터(Garbage Collector, GC)
파이썬은 주로 참조 카운팅(reference counting)과 순환 감지(cycle detection) 기반의 가비지 컬렉션을 사용한다.
참조 카운팅(Reference Counting)
파이썬 객체는 생성될 때마다 참조 카운트를 가진다. 객체가 참조될 때 카운트가 증가하고, 참조가 해제될 때 카운트가 감소한다. 참조 카운트가 0이 되면 해당 객체가 더 이상 사용되지 않는다는 것을 뜻하므로, 메모리에서 해제시킨다. 하지만 이 방식은 순환참조라는 문제를 해결하진 못한다. 순환참조란 객체가 서로를 참조해서 참조 카운트가 0이 되지않아 실제로 사용하지 않지만 메모리 해제가 불가능한 상황을 말한다.
순환참조 예시
a = []
b = []
a.append(b)# a가 b를 참조
b.append(a)# b가 a를 참조
Python
복사
순환감지(Cycle Detection)
이런 순환 참조를 해결하기 위해 파이썬의 gc 모듈은 모든 객체의 참조 그래프를 분석해 순환 참조가 있는지 감지하여 회수한다. 이런 순환 감지는 주기적으로 수행되는데, 모든 객체를 매번 스캔하는 것이 비효율 적이므로 파이썬 내부에서 객체들을 세대별로 관리하는 방식으로 동작한다.
세대별 관리(Generational Garbage Collection)
파이썬에서는 객체를 세 개의 세대로 나누어 관리한다.
•
0세대 (가장 최근 생성된 객체, 일정 수 이상의 객체가 생성되면 가비지 컬렉션 실행)
•
1세대 (0세대 에서 살아남은 객체)
•
2세대 (가장 오래된 객체로 제일 적게 검사)
최근에 생성된 객체들은 짧은 주기로 검사를 받고 오래된 객체들은 더 긴 주기로 검사한다. 0세대를 스캔하여 실제 사용하지 않는 객체를 수거하고 여기서 수거되지 않은 객체는 1세대로 간다. 오래된 객체는 생존 가능성이 높기 때문에 주기를 늘려 검사를 적게 한다.
성능저하와 최적화
가비지 컬렉터는 특정상황에서 성능 저하를 초래 할 수 있다.
•
순환참조가 많은 경우
◦
추가적인 메모리 그래프를 탐색해서 순환 검출을 해야 하기 때문에 시간이 오래 걸릴 수 있음
•
객체 생성 및 소멸이 빈번한 경우
◦
매우 빈번하게 객체가 생성되면 0세대에서 계속해서 가비지 컬렉션이 발생
•
모든 실행 중단 현상
◦
가비지 컬렉션 실행 시 모든 실행중인 스레드가 일시적으로 중단됨
◦
안전하게 메모리를 관리하고 일관성을 유지하려면 작업을 잠시 멈춰 메모리에 접근하는 것을 방지해야 하기 때문
◦
멀티스레드 프로그램에서 특히 성능 저하가 두드러질 수 있음
실무에서 메모리 누수나 성능 저하 문제를 해결할 때는 가비지 컬렉터에 의존하기보다, 순환 참조를 최소화하고, 필요한 경우 객체를 명시적으로 해제하거나, 객체 재사용을 적극적으로 고려해야 한다. 파이썬의 gc 모듈을 통해 가비지 컬렉션의 세부적인 정보를 확인하고 필요할때는 수동으로 제어를 할 수 있다. 예를 들어 특정 시점에 가비지 컬렉터를 비활성화 했다가, 작업이 끝난 후 다시 활성화하는 방식으로 메모리를 관리 할 수도 있다.
이번 글로 파이썬의 메모리 관리 방식과 가비지 컬렉터에 대해 알아보았다. 파이썬은 자동화된 메모리 관리 덕분에 개발자에게 많은 편의를 제공한다. 하지만 특정 상황에서는 가비지 컬렉터의 동작 방식이 성능에 영향을 미칠 수 있으므로, 이를 이해하고 적절히 최적화하는 것이 중요하다.