"서비스가 점점 느려지더니 결국 서버가 다운됐습니다."
어느 날 갑자기 메모리 사용량이 계속 증가하다가 서버가 멈춰버린 경험이 있으신가요? 분명 코드에는 문제가 없어 보이는데, 시간이 지날수록 메모리가 해제되지 않고 계속 쌓여만 가는 상황. 이것이 바로 '메모리 누수'입니다.
파이썬은 직접 메모리를 관리해주지 않아도, 가비지 콜렉터가 자동으로 메모리를 관리해 주는 언어입니다. 가비지 콜렉터는 일정 주기로 실행되어 더 이상 사용되지 않는 객체들의 메모리를 해제해 줍니다. 이는 대부분 아주 잘 작동하지만, 가끔 문제가 생기기도 합니다.
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
: 테스트 컬렉션 관리를 위한 약한 참조
pytest
의 Pytester
클래스에서는 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
모듈 사용을 고려해 보시는 것을 추천드립니다. 긴 글 읽어주셔서 감사합니다.
참고자료
'개발 > 파이썬' 카테고리의 다른 글
LangChain 고급 컴포넌트 (Agents, Tools, LangGraph) 활용하기 🚀 - 나만의 AI 비서 만들기 #2 (0) | 2025.03.02 |
---|---|
LangChain 핵심 컴포넌트 (Prompt, Output Parser, Chain) 이해하기 🤖 - AI 비서 만들기 #1 (3) | 2025.02.15 |
성능 최적화 및 SQL 활용 🚀 - 자주 쓰는 명령어로 배우는 Polars #6 (0) | 2025.01.25 |
데이터 결합과 재구조화 🔄 - 자주 쓰는 명령어로 배우는 Polars #5 (1) | 2024.12.08 |
Polars로 데이터 그룹화와 집계 📊 - 자주쓰는 명령어로 배우는 Polars #4 (0) | 2024.11.23 |