본문 바로가기

개발/파이썬

가비지 컬렉터가 있는데 왜 메모리 누수가? weakref로 해결하기

반응형
"서비스가 점점 느려지더니 결국 서버가 다운됐습니다."

어느 날 갑자기 메모리 사용량이 계속 증가하다가 서버가 멈춰버린 경험이 있으신가요? 분명 코드에는 문제가 없어 보이는데, 시간이 지날수록 메모리가 해제되지 않고 계속 쌓여만 가는 상황. 이것이 바로 '메모리 누수'입니다.

파이썬은 직접 메모리를 관리해주지 않아도, 가비지 콜렉터가 자동으로 메모리를 관리해 주는 언어입니다. 가비지 콜렉터는 일정 주기로 실행되어 더 이상 사용되지 않는 객체들의 메모리를 해제해 줍니다. 이는 대부분 아주 잘 작동하지만, 가끔 문제가 생기기도 합니다.

weakref 알아보기

위와 같은 상황은 객체들끼리 서로 물리고 물리는 관계 (예를 들어 list나 dict에 어떤 객체를 저장)에 자주 발생합니다. 이럴 때, 파이썬의 weakref 모듈을 사용한다면 메모리 누수를 예방할 수 있습니다. weakref를 통해 객체를 참조한다면, 가비지 컬렉터는 해당 참조를 무시하고 객체를 정리할 수 있습니다.

1. 약한 참조 만들기: weakref.ref()weakref.proxy()

weakref.ref()

객체에 대한 '약한 연결'을 만들어 줍니다. 연결된 객체를 보려면 ()를 써야 하고, 객체가 사라졌으면 None을 응답합니다.

import weakref

class MyClass:
    def __init__(self, name):
        self.name = name
        print(f"'{self.name}' 생성!")
    def __del__(self):
        print(f"'{self.name}' 삭제됨!")

obj = MyClass("내 객체")
weak_obj = weakref.ref(obj)

print(weak_obj())  # <__main__.MyClass object at 0x...> (객체가 살아있다면)

del obj  # '내 객체'에 대한 강한 참조가 사라짐
# 출력: '내 객체' 삭제됨!

print(weak_obj())  # None (객체가 사라졌으니)

위의 예제를 보면 weak_obj가 여전히 참조를 가지고 있으나 가비지콜렉터에 의해 정리되는 것을 확인할 수 있습니다. 만약 위의 코드에서 weakref를 쓰지 않았다면, 아래와 같이 객체가 정리되지 않습니다.

obj = MyClass("내 객체")
obj2 = obj

del obj
# 아직 obj2에 대한 참조가 남아있어서 메모리 정리가 되지 않음

del obj2  # 모든 강한참조가 사라짐
# 출력: '내 객체' 삭제됨!

 

weakref.proxy()

객체처럼 바로 쓸 수 있는 '대리인'을 만들어서, () 없이도 객체의 속성이나 메서드를 쓸 수 있습니다. 원래 객체가 사라지면 에러(ReferenceError)가 발생합니다.

import weakref

class MyClass:
    def do_something(self):
        print("뭔가 한다!")

obj = MyClass()
proxy_obj = weakref.proxy(obj)

proxy_obj.do_something()  # 출력: 뭔가 한다!

del obj

proxy_obj.do_something()

# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# ReferenceError: weakly-referenced object no longer exists

2. 똑똑한 컬렉션: WeakKeyDictionary, WeakValueDictionary, WeakSet

아까 메모리 누수 문제는 객체들끼리 서로 물리는 관계에서 많이 발생한다고 한 거 기억하시나요? 이런 상황은 보통 객체들을 컬렉션에 저장(캐시)해두는 상황에서 발생합니다. 다른 곳에서는 객체의 참조가 사라졌지만, 컬렉션에서 객체들의 참조를 계속 가지고 있는 경우입니다. 이럴 때 위에서 다룬 weakref.ref를 사용할 수도 있으나 weakref 모듈에서는 편의를 위한 컬렉션을 제공하고 있습니다.

WeakKeyDictionary: 딕셔너리의 가 약한 참조! 키로 쓰인 객체가 사라지면 딕셔너리에서 자동으로 없어집니다.

import weakref
import gc

class MyKey:
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return f"MyKey({self.name})"

key1 = MyKey("키1")
my_dict = weakref.WeakKeyDictionary()
my_dict[key1] = "값1"

print(key1 in my_dict)  # True

del key1
gc.collect()  # 가비지 컬렉션 실행

print(len(my_dict.keys()))  # 0

WeakValueDictionary: 딕셔너리의 이 약한 참조! 값으로 쓰인 객체가 사라지면 딕셔너리에서 자동으로 없어집니다.

import weakref
import gc

class MyValue:
    def __init__(self, data):
        self.data = data
        print(f"MyValue({data}) 생성!")
    def __del__(self):
        print(f"MyValue({self.data}) 삭제됨!")

value1 = MyValue("데이터1")
my_dict = weakref.WeakValueDictionary()
my_dict["하나"] = value1

del value1
gc.collect()  # 가비지 컬렉션 실행
# 출력: MyValue(데이터1) 삭제됨!

print("하나" in my_dict)  # False

WeakSet: Set 안의 요소들이 약한 참조! 요소가 사라지면 Set에서 자동으로 없어집니다.

import weakref
import gc

class MyItem:
    def __init__(self, name):
        self.name = name
        print(f"MyItem({name}) 생성!")
    def __del__(self):
        print(f"MyItem({self.name}) 삭제됨!")

item1 = MyItem("아이템1")
my_set = weakref.WeakSet([item1])

del item1
gc.collect()  # 가비지 컬렉션 실행
# 출력: MyItem(아이템1) 삭제됨!

print(len(my_set))  # 0

3. 메서드에 대한 약한 참조: weakref.WeakMethod

weakref.WeakMethod(method)는 메서드에 대한 약한 참조를 만듭니다. 원래 객체가 사라지면, WeakMethod()를 호출해도 None이 나옵니다. 주로 콜백 등록 시, 객체가 사라지면 자동으로 콜백도 사라지게 하고 싶을 때 사용합니다.

import weakref
import gc

class MyClass:
    def __init__(self, name):
        self.name = name
        print(f"{self.name} 생성됨")
    def my_method(self):
        print(f"{self.name}의 메서드 호출됨!")
    def __del__(self):
        print(f"{self.name} 삭제됨")

obj = MyClass("내 객체")
weak_method = weakref.WeakMethod(obj.my_method)
print(weak_method()) # <bound method MyClass.my_method of <__main__.MyClass object at 0x1010cdd90>>
weak_method()()  # 출력: 내 객체의 메서드 호출됨!

del obj
gc.collect()  # 가비지 컬렉션 실행

print(weak_method()) # None

실행 결과:

오픈소스 내 weakref 활용 사례

이번에는 실제로 weakref 모듈이 오픈소스들에서 어떻게 사용되는지 살펴보겠습니다. weakref 모듈은 실제로 다양한 오픈소스들에서 메모리 관리를 위해 사용되고 있습니다.

1. cachetools: 약한 참조를 이용한 캐시

WeakKeyDictionary를 사용하여 캐시를 구현합니다. 이 방식은 캐시된 객체에 다른 강한 참조가 없을 경우 가비지 컬렉터가 해당 객체를 메모리에서 해제하도록 하여 메모리 효율성을 개선합니다.

소스 코드:

 

cachetools/src/cachetools/_cachedmethod.py at e497575fcb4d29dfb773824956539215db964059 · tkem/cachetools

Extensible memoizing collections and decorators. Contribute to tkem/cachetools development by creating an account on GitHub.

github.com

 

2. blinker: 이벤트 핸들러의 약한 참조 관리

이벤트 알림 시스템에서 구독자(옵저버) 객체를 관리하기 위해 weakref를 사용합니다. WeakMethod를 통해 구독 객체에 대한 약한 참조를 유지함으로써, 해당 객체가 소멸 시 자동으로 정리되어 메모리 누수를 방지합니다.

소스 코드:

 

blinker/src/blinker/_utilities.py at c757984aaf08f229bab8aa63df7bb4b48b0be943 · pallets-eco/blinker

A fast Python in-process signal/event dispatching system. - pallets-eco/blinker

github.com

 

3. pytest: 테스트 컬렉션 관리를 위한 약한 참조

pytestPytester 클래스에서는 WeakKeyDictionary를 사용하여 테스트 컬렉션을 관리합니다. _mod_collections 속성은 Collector 객체를 키로 하고 해당 컬렉터가 수집한 테스트 아이템들의 리스트를 값으로 저장합니다. 이때 WeakKeyDictionary를 사용함으로써 컬렉터 객체가 더 이상 사용되지 않을 때 자동으로 딕셔너리에서 제거되어 메모리 누수를 방지합니다.

 

pytest/src/_pytest/pytester.py at 85a76b84296263eb6b13b59dd0641ff2f920dae2 · pytest-dev/pytest

The pytest framework makes it easy to write small tests, yet scales to support complex functional testing - pytest-dev/pytest

github.com

 

4. Django: 시그널 시스템에서의 약한 참조 활용

Django의 시그널(Signal) 시스템은 weakref 모듈의 다양한 기능을 종합적으로 활용하는 대표적인 사례입니다. 메모리 누수 방지와 성능 최적화를 위해 여러 종류의 약한 참조를 사용합니다.

1. WeakKeyDictionary를 이용한 캐싱

class Signal:
    def __init__(self, use_caching=False):
        # sender별 receiver 캐시를 WeakKeyDictionary로 관리
        self.sender_receivers_cache = weakref.WeakKeyDictionary() if use_caching else {}

2. 다양한 약한 참조 타입 활용

def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
    if weak:
        ref = weakref.ref
        receiver_object = receiver
        # 바운드 메서드인 경우 WeakMethod 사용
        if hasattr(receiver, "__self__") and hasattr(receiver, "__func__"):
            ref = weakref.WeakMethod
            receiver_object = receiver.__self__
        receiver = ref(receiver)
        weakref.finalize(receiver_object, self._flag_dead_receivers)

소스 코드:

 

django/django/dispatch/dispatcher.py at b373721af0e5c3de0986977ac07e3ad55061ecbe · django/django

The Web framework for perfectionists with deadlines. - django/django

github.com

 

마치며

이번 포스트에서는 weakref 모듈의 기본 원리와 함께 오픈소스 프로젝트에서의 적용 사례를 살펴보았습니다. Django, FastAPI 등 주요 프레임워크들이나 Pydantic 같은 유명한 라이브러리들 모두 weakref를 잘 활용하고 있습니다.

여러분들의 프로그램에도 메모리 누수 문제가 발생한다면, weakref 모듈 사용을 고려해 보시는 것을 추천드립니다. 긴 글 읽어주셔서 감사합니다.

 

참고자료

반응형