본문 바로가기
프로그래밍/파이썬

파이썬 - 메모리 관리와 가비지 컬렉션

by ennak 2024. 12. 1.
반응형

파이썬의 메모리 관리와 가비지 컬렉션

파이썬은 개발자 친화적인 언어로 널리 알려져 있다. 이러한 특성 중 하나가 바로 자동화된 메모리 관리 시스템이다. 파이썬 프로그래머는 메모리 할당과 해제에 대해 직접적으로 신경 쓸 필요가 없다. 그러나 이 편리함 뒤에는 복잡한 메커니즘이 숨어있다. 이 글에서는 파이썬의 메모리 관리 방식과 가비지 컬렉션 시스템에 대해 자세히 살펴본다.


파이썬의 메모리 관리 기본 원리

파이썬의 메모리 관리는 크게 두 가지 핵심 요소로 구성된다: 참조 카운팅과 가비지 컬렉션이다. 이 두 메커니즘은 서로 보완적으로 작동하여 메모리를 효율적으로 관리한다.


참조 카운팅

참조 카운팅은 파이썬 객체가 얼마나 많은 참조를 가지고 있는지를 추적하는 방식이다. 객체가 생성될 때마다 참조 카운트는 1로 시작하며, 해당 객체에 대한 새로운 참조가 생길 때마다 1씩 증가한다. 반대로, 참조가 제거될 때마다 1씩 감소한다.

x = []  # x의 참조 카운트: 1
y = x   # x의 참조 카운트: 2
del y   # x의 참조 카운트: 1

순환 참조 문제

순환 참조는 두 개 이상의 객체가 서로를 참조하는 상황을 말한다. 이 경우, 참조 카운팅만으로는 메모리 누수가 발생할 수 있다.

class Node:
    def __init__(self):
        self.ref = None

a = Node()
b = Node()
a.ref = b
b.ref = a

del a
del b

위 코드에서 ab는 서로를 참조하고 있다. del adel b를 실행한 후에도, 두 객체의 참조 카운트는 1로 유지되어 메모리에서 해제되지 않는다. 이러한 문제를 해결하기 위해 파이썬은 가비지 컬렉션 메커니즘을 도입했다.


가비지 컬렉션

파이썬의 가비지 컬렉터는 참조 카운팅으로 해결할 수 없는 순환 참조 문제를 처리한다. 가비지 컬렉터는 주기적으로 실행되며, 더 이상 필요하지 않은 객체를 식별하고 제거한다.


가비지 컬렉션의 작동 원리

  1. 객체 추적: 가비지 컬렉터는 모든 객체를 추적한다.
  2. 순환 참조 탐지: 객체 간의 참조 관계를 분석하여 순환 참조를 찾아낸다.
  3. 도달 가능성 확인: 루트 객체(전역 변수, 스택 프레임 등)로부터 도달 가능한 객체를 식별한다.
  4. 불필요한 객체 제거: 도달할 수 없는 객체들을 메모리에서 해제한다.

세대별 가비지 컬렉션

파이썬의 가비지 컬렉터는 세대별 수집(Generational Collection) 방식을 사용한다. 이 방식은 객체를 세 세대로 나누어 관리한다.

  1. 첫 번째 세대 (Young): 새로 생성된 객체들이 속한다. 가장 자주 검사되고 수집된다.
  2. 두 번째 세대 (Middle-aged): 첫 번째 세대에서 살아남은 객체들이 이동한다.
  3. 세 번째 세대 (Old): 오랫동안 살아남은 객체들이 속한다. 가장 적게 검사된다.

이 방식은 "대부분의 객체는 생성 직후 곧 불필요해진다"는 경험적 관찰에 기반한다. 새로운 객체들을 더 자주 검사함으로써 전체적인 가비지 컬렉션 성능을 향상시킨다.

메모리 관리 최적화 기법

파이썬의 자동화된 메모리 관리 시스템은 편리하지만, 개발자가 메모리 사용을 최적화할 수 있는 방법들이 있다.


1. 불필요한 참조 제거

큰 객체를 사용한 후에는 명시적으로 참조를 제거하는 것이 좋다. 이는 del 키워드를 사용하거나 변수에 None을 할당하여 수행할 수 있다.

large_data = process_huge_file()
# 데이터 처리
del large_data  # 또는 large_data = None

2. 제너레이터 사용

대용량 데이터를 처리할 때는 모든 데이터를 한 번에 메모리에 로드하는 대신 제너레이터를 사용하여 메모리 사용을 줄일 수 있다.

def process_large_file(filename):
    with open(filename, 'r') as file:
        for line in file:
            yield process_line(line)

for processed_line in process_large_file('huge_file.txt'):
    # 처리된 라인으로 작업

3. 약한 참조 사용

weakref 모듈을 사용하여 약한 참조를 만들 수 있다. 약한 참조는 객체의 참조 카운트를 증가시키지 않아, 캐시나 큰 객체 그래프에서 유용하게 사용될 수 있다.


import weakref

class ExpensiveObject:
    def __init__(self, value):
        self.value = value

obj = ExpensiveObject(42)
weak_ref = weakref.ref(obj)

# 객체가 여전히 존재하는 경우
print(weak_ref().value)  # 출력: 42

# 객체가 삭제된 경우
del obj
print(weak_ref())  # 출력: None

4. 슬롯 사용

클래스에 __slots__를 정의하면 인스턴스 딕셔너리 사용을 방지하고 메모리 사용을 줄일 수 있다.

class Point:
    __slots__ = ['x', 'y']

    def __init__(self, x, y):
        self.x = x
        self.y = y

# Point 인스턴스는 x와 y 속성만 가질 수 있으며, 메모리 사용이 최적화된다.

가비지 컬렉션 제어

파이썬은 개발자에게 가비지 컬렉션 프로세스를 제어할 수 있는 도구를 제공한다. gc 모듈을 사용하여 가비지 컬렉션의 동작을 세부적으로 조정할 수 있다.


1. 가비지 컬렉션 비활성화/활성화

특정 상황에서는 가비지 컬렉션을 일시적으로 비활성화하는 것이 유용할 수 있다. 예를 들어, 시간에 민감한 작업을 수행할 때 가비지 컬렉션으로 인한 지연을 방지할 수 있다.

import gc

gc.disable()  # 가비지 컬렉션 비활성화
# 시간에 민감한 작업 수행
gc.enable()   # 가비지 컬렉션 다시 활성화

2. 수동 가비지 컬렉션 실행

필요한 경우 수동으로 가비지 컬렉션을 실행할 수 있다.

gc.collect()  # 모든 세대의 가비지 컬렉션 실행
gc.collect(0)  # 첫 번째 세대만 가비지 컬렉션 실행

3. 가비지 컬렉션 임계값 조정

각 세대의 가비지 컬렉션 임계값을 조정할 수 있다. 이는 메모리 사용과 성능 간의 균형을 맞추는 데 도움이 될 수 있다.

print(gc.get_threshold())  # 현재 임계값 출력
gc.set_threshold(1000, 15, 15)  # 임계값 변경

메모리 누수 디버깅

메모리 누수는 파이썬에서도 발생할 수 있으며, 이를 디버깅하는 것은 중요한 기술이다. 몇 가지 유용한 도구와 기법을 소개한다.


1. memory_profiler 사용

memory_profiler 라이브러리를 사용하면 코드의 메모리 사용량을 라인별로 프로파일링할 수 있다.

from memory_profiler import profile

@profile
def my_func():
    a = [1] * (10 ** 6)
    b = [2] * (2 * 10 ** 7)
    del b
    return a

if __name__ == '__main__':
    my_func()

이 코드를 실행하면 각 라인의 메모리 사용량을 볼 수 있다.


2. objgraph 사용

objgraph 라이브러리는 객체 참조 그래프를 시각화하여 메모리 누수의 원인을 찾는 데 도움을 준다.

import objgraph

objgraph.show_most_common_types()
objgraph.show_growth()

이 코드는 가장 많이 생성된 객체 유형과 증가한 객체 수를 보여준다.


3. tracemalloc 사용

Python 3.4부터 제공되는 tracemalloc 모듈은 Python 객체에 의해 할당된 메모리 블록을 추적하는 데 사용된다.

import tracemalloc

tracemalloc.start()

# 코드 실행

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("[ Top 10 ]")
for stat in top_stats[:10]:
    print(stat)

이 코드는 메모리 사용량이 가장 많은 10개의 라인을 보여준다.

반응형