본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] Weakref 캐시 시스템 구축을 위한 3가지 최적화 방법과 메모리 누수 해결책

by Papa Martino V 2026. 4. 22.
728x90

Weakref 캐시
Weakref 캐시

 

애플리케이션의 규모가 커질수록 데이터 재사용을 위한 '캐싱(Caching)'은 필수적입니다. 하지만 일반적인 딕셔너리(dict)를 캐시 저장소로 사용할 경우, 캐시에 담긴 객체는 '강한 참조(Strong Reference)'로 묶여 가비지 컬렉터(GC)가 메모리를 회수하지 못하는 상황이 발생합니다. 이는 결국 시스템 전체의 메모리 부족(OOM)으로 이어지는 병목 현상을 초래합니다. 이러한 문제를 우아하게 해결할 수 있는 파이썬의 비밀 병기가 바로 weakref 모듈입니다. 본 포스팅에서는 약한 참조를 활용해 메모리 압박 없이 동작하는 지능형 캐시 시스템 설계 방법과 실무에서 마주할 수 있는 차이점들을 상세히 다루겠습니다.


1. 강한 참조(Strong Reference) vs 약한 참조(Weak Reference) 차이 분석

캐시 시스템 설계 시 어떤 참조 방식을 선택하느냐에 따라 메모리 관리 전략이 완전히 달라집니다.

항목 강한 참조 (Standard dict) 약한 참조 (weakref.WeakValueDictionary)
참조 횟수(Ref Count) 객체의 참조 횟수를 1 증가시킴 참조 횟수에 영향을 주지 않음
GC 회수 여부 캐시에 남아있는 한 회수 불가 외부 참조가 사라지면 자동으로 회수
메모리 누수 위험 수동으로 pop 하지 않으면 매우 높음 매우 낮음 (자동 메모리 관리)
주요 용도 반드시 유지되어야 하는 데이터 임시 데이터, 거대 모델 객체, 이미지 캐시
객체 접근 즉시 접근 가능 객체 생존 여부 확인 후 접근 가능

2. 실무형 Weakref 캐시 구축을 위한 7가지 해결 방법 (Sample Examples)

개발자가 실무 환경에서 즉시 복사하여 적용할 수 있는 구체적인 예제 코드들입니다.

Ex 1. WeakValueDictionary를 활용한 기본적인 객체 캐시

import weakref

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

# 외부 참조가 끊기면 자동으로 항목이 삭제되는 딕셔너리
cache = weakref.WeakValueDictionary()

def get_model(name):
    if name not in cache:
        print(f"Creating new model: {name}")
        model = LargeModel(name)
        cache[name] = model
    return cache[name]

# 사용 예시
m1 = get_model("AI_Alpha")
print(f"Cache size: {len(cache)}")
del m1  # m1을 삭제하면 cache에서도 자동으로 제거됨
print(f"Cache size after del: {len(cache)}")

Ex 2. weakref.ref 콜백을 활용한 리소스 정리 자동화

import weakref

def on_finalize(reference):
    print("The object is being collected. Closing file handles...")

class DataContainer:
    pass

obj = DataContainer()
# 객체가 소멸될 때 특정 함수를 호출하도록 예약
r = weakref.ref(obj, on_finalize)
del obj

Ex 3. WeakKeyDictionary를 이용한 객체별 메타데이터 저장

import weakref

# 객체 자체를 키로 사용하여 부가 정보를 기록할 때 유용
# 객체가 사라지면 해당 객체에 대한 메타데이터도 함께 사라짐
metadata_store = weakref.WeakKeyDictionary()

class UserSession:
    def __init__(self, user_id):
        self.user_id = user_id

session = UserSession("user_01")
metadata_store[session] = {"login_time": "2026-04-20", "ip": "127.0.0.1"}

print(f"Metadata exists: {session in metadata_store}")
del session
# 이제 metadata_store는 자동으로 비워짐

Ex 4. 프록시(Proxy)를 사용한 투명한 약한 참조 접근

import weakref

class ExpensiveComponent:
    def execute(self):
        return "Action completed"

comp = ExpensiveComponent()
# .ref()와 달리 호출 방식() 없이 바로 객체처럼 사용 가능
proxy = weakref.proxy(comp)

print(proxy.execute())
del comp
try:
    print(proxy.execute())
except ReferenceError:
    print("Component no longer exists!")

Ex 5. WeakSet을 활용한 구독자(Subscriber) 리스트 관리

import weakref

class EventPublisher:
    def __init__(self):
        # 구독자가 메모리에서 해제되면 자동으로 리스트에서 제거됨
        self.subscribers = weakref.WeakSet()

    def subscribe(self, subscriber):
        self.subscribers.add(subscriber)

    def notify(self):
        for sub in self.subscribers:
            sub.update()

Ex 6. 싱글톤 패턴과 Weakref의 결합 (메모리 효율적 싱글톤)

class DatabaseConnection:
    _instances = weakref.WeakValueDictionary()

    def __new__(cls, connection_string):
        if connection_string in cls._instances:
            return cls._instances[connection_string]
        
        instance = super(DatabaseConnection, cls).__new__(cls)
        cls._instances[connection_string] = instance
        return instance

Ex 7. finalize를 사용한 안전한 GPU 리소스 해제 레이어

import weakref

class GPUTensor:
    def __init__(self, data):
        self.data = data
        # __del__ 보다 안전한 종료 방식
        self._finalizer = weakref.finalize(self, self.release_memory, self.data)

    @staticmethod
    def release_memory(data):
        print(f"Releasing GPU memory for {data}")

tensor = GPUTensor("Raw_Data_01")
del tensor # 즉각적인 finalizer 호출 트리거

3. Weakref 캐시 시스템 구축 시 주의점 (Critical Point)

효율적인 시스템을 구축하기 위해 반드시 극복해야 할 제약 사항들입니다.

  1. 모든 객체가 약한 참조를 지원하지 않음: list, dict, int, str과 같은 빌트인 타입은 직접적인 weakref 대상이 될 수 없습니다. 이를 위해서는 해당 타입을 상속받은 사용자 정의 클래스가 필요합니다.
  2. 순환 참조와의 관계: weakref는 순환 참조 문제를 해결하는 데 도움을 주지만, finalize 내부에서 대상 객체를 참조하면 다시 강한 참조가 발생하여 영원히 해제되지 않을 수 있습니다.
  3. 멀티스레딩 환경: GC가 객체를 회수하는 시점과 파이썬 코드가 캐시에 접근하는 시점 사이에 레이스 컨디션(Race Condition)이 발생할 수 있습니다. try-except 블록으로 ReferenceError를 대비해야 합니다.

4. 결론: 언제 Weakref 캐시를 써야 해결되는가?

결론적으로 "객체의 생명 주기를 애플리케이션의 비즈니스 로직이 아닌, 메모리 가용성에 맡기고 싶을 때" weakref는 최고의 해결책이 됩니다. 특히 AI 모델 서빙, 대용량 이미지 처리, 혹은 복잡한 객체 그래프를 가진 프레임워크 설계 시 weakref 기반의 캐시는 시스템의 생존 성능을 결정짓는 핵심 요소가 될 것입니다. 강한 참조의 늪에서 벗어나, 파이썬 가비지 컬렉터와 공존하는 스마트한 캐시 시스템을 구축해 보시기 바랍니다.


참고 출처 및 문헌

  • Python Official Documentation - Standard Library: weakref module
  • Fluent Python (2nd Edition) - Luciano Ramalho (O'Reilly Media)
  • High Performance Python - Micha Gorelick & Ian Ozsvald
  • PEP 205 – Weak References
728x90