모듈 캐싱과 sys.modules: 한 번만 로드되는 이유
Python sys.modules 캐시 동작 원리, importlib.reload() 한계와 올바른 사용법, __pycache__ 바이트코드 캐시 구조를 설명합니다.
지난 글에서 __name__ 이디엄을 살펴봤습니다. 이번에는 import 문이 같은 모듈을 여러 번 써도 파일이 한 번만 실행되는 이유, 즉 모듈 캐싱 메커니즘을 알아봅니다.
sys.modules: 캐시의 핵심
sys.modules는 모듈 이름을 키, 모듈 객체를 값으로 하는 딕셔너리입니다. Python이 import를 실행할 때 가장 먼저 이곳을 확인합니다.
import sys
# import 전
print("json" in sys.modules) # False
import json
# import 후
print("json" in sys.modules) # True
print(sys.modules["json"]) # <module 'json' from '...'>
같은 모듈을 100번 임포트해도 sys.modules에 있으면 파일 실행 없이 즉시 반환됩니다. 이 덕분에 임포트가 빠르고, 모듈 레벨 전역 상태가 프로그램 전체에서 공유됩니다.
싱글턴 효과
모듈 레벨 객체가 캐시 덕분에 사실상 싱글턴처럼 동작합니다.
# counter.py
count = 0
def increment():
global count
count += 1
# a.py
import counter
counter.increment()
print(counter.count) # 1
# b.py
import counter # 동일한 객체! (새 로드 아님)
print(counter.count) # 1 (a.py에서 증가시킨 값 그대로)
설정 모듈, 로깅 설정, 레지스트리 등을 모듈 레벨에서 관리하는 패턴이 이를 활용합니다.
importlib.reload()
파일을 수정하고 재시작 없이 변경사항을 적용하려면 reload()를 씁니다.
import importlib
import mymod
# mymod.py 파일 수정 후
importlib.reload(mymod)
reload()는 모듈 파일을 다시 실행하고 모듈 객체의 속성을 갱신합니다. 그러나 중요한 제한이 있습니다.
from module import name 으로 이미 가져온 이름은 갱신되지 않습니다.
from mymod import greet # greet는 구버전 함수 객체를 직접 가리킴
importlib.reload(mymod) # 모듈 재로드
greet() # 여전히 구버전! (greet 변수가 새 함수를 가리키지 않음)
mymod.greet() # 새버전 (모듈 속성 접근이라 갱신된 것)
reload() 후에는 from 임포트로 가져온 이름을 다시 바인딩해야 합니다.
importlib.reload(mymod)
from mymod import greet # 다시 바인딩
greet() # 이제 새버전
sys.modules 직접 조작
테스트나 플러그인 시스템에서 sys.modules를 직접 건드리는 일이 있습니다.
import sys
# 모듈 캐시 강제 삭제 → 다음 import 시 재로드
del sys.modules["mymod"]
# None 등록 → import 시 ImportError 강제 발생
# (테스트에서 "이 패키지가 없는 환경" 시뮬레이션)
sys.modules["numpy"] = None
try:
import numpy # ImportError: import of 'numpy' halted
except ImportError:
print("numpy 없음")
가짜 모듈을 등록하는 것도 가능합니다. pytest의 monkeypatch 픽스처나 unittest.mock.patch가 이 방식으로 모듈을 교체합니다.
__pycache__와 .pyc 파일
Python은 .py 파일을 처음 임포트할 때 바이트코드로 컴파일해 __pycache__/ 디렉터리에 저장합니다.
myapp/
__pycache__/
utils.cpython-312.pyc ← 컴파일된 바이트코드
utils.py
파일명에 cpython-312처럼 인터프리터 버전이 포함되어, CPython 3.12로 컴파일된 파일은 3.11에서 사용되지 않습니다.
다음 번에 임포트할 때 Python은:
.py파일의 수정 시각과 크기를.pyc에 기록된 값과 비교- 일치하면
.pyc를 직접 로드(컴파일 생략 → 빠름) - 불일치하면
.py를 다시 컴파일
# .pyc 생성 억제
python -B script.py
# 또는 환경변수
PYTHONDONTWRITEBYTECODE=1 python script.py
.pyc 파일을 .gitignore에 추가하는 것이 관례입니다(__pycache__/ 디렉터리 전체).
.pyc 직접 배포
소스를 숨기고 바이트코드만 배포할 수도 있습니다. compileall 모듈로 일괄 컴파일합니다.
python -m compileall myapp/
# __pycache__/*.pyc 생성
# .py 파일 없이 .pyc만 배포
# 단, 디컴파일 도구로 역분석이 가능하므로 완전한 난독화는 아님
지난 글: if name == “main” 이디엄
다음 글: 조건부 임포트
읽어주셔서 감사합니다. 😊