Python Import System Deepdive
Introduction
Python의 import 시스템은 모듈을 로드하고 관리하는 핵심 메커니즘입니다. 단순히 import 키워드를 사용하는 것을 넘어, Python이 어떻게 모듈을 찾고, 로드하고, 캐싱하는지 이해하면 더 효율적인 코드를 작성하고 복잡한 문제를 디버깅할 수 있습니다.
📌 이 글에서 다루는 내용
🔍 핵심 주제
- Import 메커니즘: Python이 모듈을 찾고 로드하는 전체 과정
- sys.modules와 캐싱: 모듈 캐시 시스템의 동작 원리
- importlib 심화: Python의 import 구현 내부 살펴보기
- Meta Path Finder: 커스텀 import 훅 구현하기
🎯 주요 내용
-
Import 기초
- import vs from import 차이점
- 절대 경로 vs 상대 경로 import
__init__.py의 역할과 패키지 구조
-
Import 내부 동작
- sys.path와 모듈 검색 경로
- sys.modules 캐시 메커니즘
- import 프로세스의 5단계
-
고급 Import 기법
- importlib 모듈 활용
- 동적 import (__import__, importlib.import_module)
- Lazy import로 시작 시간 최적화
- Meta path finder 커스터마이징
-
실전 활용
- 순환 import 문제 해결
- 플러그인 시스템 구현
- Import hook을 활용한 자동 reload
💡 이런 분들께 추천
- Python 모듈 시스템을 깊이 이해하고 싶은 개발자
- Import 관련 오류를 자주 겪는 분
- 플러그인 아키텍처를 설계하는 엔지니어
- Python 패키지 개발자
Import 기초 이해
import vs from import
Python에서 모듈을 가져오는 두 가지 주요 방법이 있습니다:
# 방법 1: import 문
import math
print(math.pi) # 3.141592653589793
# 방법 2: from import 문
from math import pi
print(pi) # 3.141592653589793
# 방법 3: 별칭 사용
import numpy as np
from collections import Counter as C
차이점:
import module: 모듈 전체를 가져오고, 모듈 이름으로 접근from module import name: 모듈에서 특정 객체만 가져와 직접 접근- 네임스페이스 관리 측면에서
import module이 더 명확함
절대 경로 vs 상대 경로 Import
# 절대 경로 import (권장)
from mypackage.subpackage import module
from mypackage.utils import helper
# 상대 경로 import (패키지 내부에서만 사용)
from . import sibling_module # 같은 디렉토리
from .. import parent_module # 부모 디렉토리
from ..sibling import cousin # 부모의 다른 자식
프로젝트 구조 예시:
mypackage/
├── __init__.py
├── module_a.py
├── module_b.py
└── subpackage/
├── __init__.py
├── module_c.py
└── module_d.py
# mypackage/subpackage/module_c.py
from .. import module_a # 상대 경로
from mypackage import module_b # 절대 경로 (권장)
__init__.py의 역할
__init__.py 파일은 디렉토리를 Python 패키지로 만듭니다:
# mypackage/__init__.py
# 패키지 초기화 코드
print("mypackage 로드됨")
# 하위 모듈을 패키지 레벨로 노출
from .module_a import ClassA
from .module_b import function_b
# __all__로 "from package import *" 제어
__all__ = ['ClassA', 'function_b']
# 패키지 메타데이터
__version__ = '1.0.0'
__author__ = 'Your Name'
Python 3.3+ Namespace Packages:
Python 3.3부터는 __init__.py 없이도 패키지 생성 가능 (PEP 420):
namespace_package/
├── subpackage_a/
│ └── module.py
└── subpackage_b/
└── module.py
Import 내부 동작 메커니즘
sys.path와 모듈 검색
Python은 sys.path에 정의된 경로에서 모듈을 검색합니다:
import sys
# sys.path 확인
for path in sys.path:
print(path)
# 출력 예시:
# /current/working/directory
# /usr/lib/python3.11
# /usr/lib/python3.11/site-packages
# ...
sys.path 순서:
- 현재 실행 중인 스크립트의 디렉토리
PYTHONPATH환경 변수- 표준 라이브러리 디렉토리
- site-packages 디렉토리 (pip로 설치한 패키지)
동적으로 경로 추가:
import sys
import os
# 프로젝트 루트를 sys.path에 추가
project_root = os.path.dirname(os.path.abspath(__file__))
if project_root not in sys.path:
sys.path.insert(0, project_root)
# 이제 프로젝트 루트 기준으로 import 가능
from mypackage import module
sys.modules 캐시 시스템
Python은 한 번 로드된 모듈을 sys.modules에 캐싱합니다:
import sys
# 모듈 import
import math
# sys.modules에서 확인
print('math' in sys.modules) # True
print(sys.modules['math']) # <module 'math' ...>
# 같은 모듈을 다시 import해도 캐시에서 가져옴
import math # 실제로 다시 로드되지 않음
캐시 제어:
import sys
import importlib
# 모듈 언로드
if 'mymodule' in sys.modules:
del sys.modules['mymodule']
# 모듈 reload
import mymodule
importlib.reload(mymodule) # 코드 변경 후 다시 로드
# 주의: reload는 이미 import된 객체에 영향 없음
from mymodule import MyClass
# mymodule을 reload해도 이미 가져온 MyClass는 변경 안 됨
Import 프로세스 5단계
Python이 import mypackage.module을 실행할 때:
- Cache Check:
sys.modules에서 모듈 확인 - Find Module: Finder로 모듈 위치 찾기
- Create Module: 모듈 객체 생성
- Execute Module: 모듈 코드 실행
- Cache Module:
sys.modules에 저장
import sys
import importlib.util
def manual_import(module_name, file_path):
"""수동으로 모듈 import 과정 구현"""
# 1. Cache check
if module_name in sys.modules:
return sys.modules[module_name]
# 2-3. Find and create module
spec = importlib.util.spec_from_file_location(module_name, file_path)
module = importlib.util.module_from_spec(spec)
# 4. Cache module (실행 전에 캐시하여 순환 import 방지)
sys.modules[module_name] = module
# 5. Execute module
spec.loader.exec_module(module)
return module
# 사용 예시
my_module = manual_import('my_module', '/path/to/my_module.py')
sys.meta_path와 Import Hook 시스템
sys.meta_path의 구조
sys.meta_path는 Python이 모듈을 찾을 때 사용하는 finder 객체들의 리스트입니다. Import 문이 실행되면 Python은 이 리스트를 순회하면서 모듈을 찾습니다.
import sys
# 기본 meta_path 확인
for finder in sys.meta_path:
print(f"{finder.__class__.__name__}: {finder}")
# 출력 예시:
# BuiltinImporter: <class '_frozen_importlib.BuiltinImporter'>
# FrozenImporter: <class '_frozen_importlib.FrozenImporter'>
# PathFinder: <class '_frozen_importlib_external.PathFinder'>
기본 Finder들의 역할:
- BuiltinImporter: 내장 모듈(sys, builtins 등) 처리
- FrozenImporter: 동결(frozen) 모듈 처리
- PathFinder: sys.path 기반 파일 시스템 모듈 검색
Import 검색 순서:
graph TD
A[import 문 실행] --> B{sys.modules 확인}
B -->|캐시 존재| Z[캐시된 모듈 반환]
B -->|캐시 없음| C[sys.meta_path 순회]
C --> D[BuiltinImporter.find_spec]
D -->|발견| E[Loader 실행]
D -->|미발견| F[FrozenImporter.find_spec]
F -->|발견| E
F -->|미발견| G[PathFinder.find_spec]
G -->|발견| E
G -->|미발견| H[ImportError]
E --> I[모듈 생성 및 실행]
I --> J[sys.modules에 캐시]
J --> Z
importlib 모듈 완전 분석
importlib 주요 컴포넌트
import importlib
import importlib.util
import importlib.abc
import importlib.machinery
# 1. importlib.import_module - 동적 import
module = importlib.import_module('json')
# 2. importlib.util - 유틸리티 함수들
spec = importlib.util.find_spec('os')
print(f"모듈 이름: {spec.name}")
print(f"위치: {spec.origin}")
print(f"Loader: {spec.loader}")
# 3. importlib.abc - Abstract Base Classes
# MetaPathFinder, Loader 등의 추상 클래스 제공
# 4. importlib.machinery - Low-level 구현체
# SourceFileLoader, ExtensionFileLoader 등
ModuleSpec 객체 상세
ModuleSpec은 모듈의 메타데이터를 담고 있는 핵심 객체입니다:
import importlib.util
# ModuleSpec 생성 방법들
spec1 = importlib.util.find_spec('json') # 표준 라이브러리
spec2 = importlib.util.spec_from_file_location(
'mymodule',
'/path/to/mymodule.py'
)
# ModuleSpec 속성들
print(f"이름: {spec1.name}") # 'json'
print(f"로더: {spec1.loader}") # SourceFileLoader
print(f"위치: {spec1.origin}") # /usr/lib/python3.11/json/__init__.py
print(f"서브모듈: {spec1.submodule_search_locations}") # ['/usr/lib/python3.11/json']
print(f"캐시: {spec1.cached}") # __pycache__ 경로
print(f"부모: {spec1.parent}") # 패키지의 경우 부모 패키지명
동적 Import 고급 기법
import importlib.util
import sys
# 방법 1: importlib.import_module (간단)
def simple_dynamic_import(module_name):
"""간단한 동적 import"""
return importlib.import_module(module_name)
# 방법 2: spec 기반 (세밀한 제어)
def advanced_dynamic_import(module_name, file_path):
"""파일 경로로부터 모듈 로드"""
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec is None:
raise ImportError(f"Cannot load {module_name} from {file_path}")
module = importlib.util.module_from_spec(spec)
# 실행 전에 sys.modules에 추가 (순환 import 방지)
sys.modules[module_name] = module
try:
spec.loader.exec_module(module)
except Exception as e:
# 실패 시 캐시에서 제거
del sys.modules[module_name]
raise
return module
# 방법 3: 소스 코드로부터 직접 로드
def import_from_source(module_name, source_code):
"""소스 코드 문자열로부터 모듈 생성"""
import types
module = types.ModuleType(module_name)
module.__file__ = f"<{module_name}>"
# 소스 코드 실행
exec(source_code, module.__dict__)
sys.modules[module_name] = module
return module
# 사용 예시
source = """
def hello():
return "Hello from dynamic module!"
class DynamicClass:
value = 42
"""
dynamic_mod = import_from_source('dynamic_module', source)
print(dynamic_mod.hello()) # "Hello from dynamic module!"
print(dynamic_mod.DynamicClass.value) # 42
플러그인 시스템 고급 구현
import importlib.util
import importlib.abc
import sys
from pathlib import Path
from typing import Dict, Any, Type
class PluginRegistry:
"""플러그인 등록 및 관리 시스템"""
def __init__(self):
self.plugins: Dict[str, Any] = {}
self._plugin_base_class = None
def set_base_class(self, base_class: Type):
"""플러그인이 상속해야 할 기본 클래스 지정"""
self._plugin_base_class = base_class
def discover_plugins(self, plugin_dir: str):
"""플러그인 디렉토리 탐색 및 로드"""
plugin_path = Path(plugin_dir)
if not plugin_path.exists():
raise ValueError(f"Plugin directory not found: {plugin_dir}")
for file_path in plugin_path.glob('*.py'):
if file_path.stem.startswith('_'):
continue
module_name = f"plugins.{file_path.stem}"
self._load_plugin(module_name, str(file_path))
def _load_plugin(self, module_name: str, file_path: str):
"""개별 플러그인 로드"""
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec is None or spec.loader is None:
print(f"Warning: Cannot load {module_name}")
return
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
spec.loader.exec_module(module)
# 플러그인 클래스 찾기
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (isinstance(attr, type) and
self._plugin_base_class and
issubclass(attr, self._plugin_base_class) and
attr is not self._plugin_base_class):
# 플러그인 등록
plugin_instance = attr()
self.plugins[attr_name] = plugin_instance
print(f"Loaded plugin: {attr_name}")
def get_plugin(self, name: str):
"""플러그인 가져오기"""
return self.plugins.get(name)
def execute_all(self, method_name: str, *args, **kwargs):
"""모든 플러그인의 특정 메서드 실행"""
results = {}
for name, plugin in self.plugins.items():
if hasattr(plugin, method_name):
results[name] = getattr(plugin, method_name)(*args, **kwargs)
return results
# 사용 예시
class PluginBase:
"""플러그인 기본 클래스"""
def process(self, data):
raise NotImplementedError
# 플러그인 등록 및 사용
registry = PluginRegistry()
registry.set_base_class(PluginBase)
registry.discover_plugins('./plugins')
# 모든 플러그인 실행
results = registry.execute_all('process', data={'input': 'test'})
Lazy Import 최적화 완전 가이드
왜 Lazy Import인가?
import time
# 문제: 무거운 모듈들을 모두 미리 로드
start = time.time()
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
print(f"Import 시간: {time.time() - start:.2f}초") # 5-10초 소요
# 해결: 실제 사용 시점에만 로드
# 애플리케이션 시작 시간: 0.1초
Lazy Import 효과 측정:
import time
import sys
def measure_import_time(module_name):
"""모듈 import 시간 측정"""
# 캐시 제거
if module_name in sys.modules:
del sys.modules[module_name]
start = time.time()
__import__(module_name)
elapsed = time.time() - start
return elapsed
# 무거운 모듈들의 import 시간 측정
heavy_modules = ['pandas', 'numpy', 'matplotlib', 'scipy']
for module in heavy_modules:
try:
elapsed = measure_import_time(module)
print(f"{module}: {elapsed:.3f}초")
except ImportError:
print(f"{module}: 설치되지 않음")
Lazy Import 구현 방법 비교
# 방법 1: 함수 내부 import (가장 간단)
def process_with_pandas(data):
import pandas as pd # 함수 호출 시에만 로드
return pd.DataFrame(data)
# 방법 2: __getattr__ 활용 (모듈 레벨)
# mymodule.py
def __getattr__(name):
if name == 'pd':
import pandas
globals()['pd'] = pandas
return pandas
raise AttributeError(f"module has no attribute '{name}'")
# 방법 3: LazyLoader 활용
import importlib.util
import sys
def lazy_import_v1(module_name):
"""LazyLoader를 사용한 lazy import"""
spec = importlib.util.find_spec(module_name)
loader = importlib.util.LazyLoader(spec.loader)
spec.loader = loader
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
loader.exec_module(module)
return module
# 사용
pandas = lazy_import_v1('pandas')
# 이 시점에서는 pandas가 로드되지 않음
df = pandas.DataFrame([1, 2, 3]) # 여기서 실제 로드됨
프로덕션급 Lazy Import 구현
import importlib
import sys
from typing import Any, Optional
import threading
class LazyImporter:
"""Thread-safe lazy import 구현"""
def __init__(self, module_name: str):
self._module_name = module_name
self._module: Optional[Any] = None
self._lock = threading.Lock()
def _load(self):
"""실제 모듈 로드 (thread-safe)"""
if self._module is not None:
return
with self._lock:
# Double-check locking pattern
if self._module is not None:
return
print(f"Loading {self._module_name}...")
self._module = importlib.import_module(self._module_name)
def __getattr__(self, name: str):
"""속성 접근 시 모듈 로드"""
self._load()
return getattr(self._module, name)
def __dir__(self):
"""dir() 지원"""
self._load()
return dir(self._module)
def __repr__(self):
if self._module is None:
return f"<LazyImporter for '{self._module_name}' (not loaded)>"
return f"<LazyImporter for '{self._module_name}' (loaded)>"
# 사용 예시
pd = LazyImporter('pandas')
np = LazyImporter('numpy')
print(pd) # <LazyImporter for 'pandas' (not loaded)>
# 실제 사용 시에만 로드됨
df = pd.DataFrame([1, 2, 3]) # 이때 pandas 로드
print(pd) # <LazyImporter for 'pandas' (loaded)>
Lazy Import를 위한 데코레이터
import functools
import importlib
def lazy_import_decorator(*module_names):
"""함수 실행 전에 필요한 모듈을 lazy import하는 데코레이터"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 필요한 모듈들 import
modules = {}
for name in module_names:
if name not in func.__globals__:
modules[name] = importlib.import_module(name)
func.__globals__[name] = modules[name]
return func(*args, **kwargs)
return wrapper
return decorator
# 사용 예시
@lazy_import_decorator('pandas', 'numpy')
def analyze_data(data):
# pandas와 numpy는 이 함수 호출 시 자동 import됨
import pandas as pd # type hint를 위해 여전히 필요
import numpy as np
df = pd.DataFrame(data)
return np.mean(df.values)
# 함수 호출 시에만 pandas, numpy 로드
result = analyze_data([[1, 2], [3, 4]])
애플리케이션 시작 시간 최적화 실전
import time
import sys
class StartupOptimizer:
"""애플리케이션 시작 시간 최적화"""
def __init__(self):
self.import_times = {}
self.lazy_modules = {}
def profile_imports(self):
"""현재 import된 모듈들의 로딩 시간 프로파일"""
import importlib.util
print("=== Import Time Profile ===")
for module_name in sorted(sys.modules.keys()):
if module_name.startswith('_'):
continue
# 모듈 크기 추정
module = sys.modules[module_name]
if hasattr(module, '__file__') and module.__file__:
size = len(dir(module))
print(f"{module_name}: {size} attributes")
def suggest_lazy_imports(self, threshold: int = 50):
"""Lazy import로 전환할 모듈 제안"""
print(f"\n=== Lazy Import 권장 모듈 (속성 {threshold}개 이상) ===")
candidates = []
for module_name, module in sys.modules.items():
if module_name.startswith('_'):
continue
if hasattr(module, '__file__') and module.__file__:
attr_count = len(dir(module))
if attr_count > threshold:
candidates.append((module_name, attr_count))
for name, count in sorted(candidates, key=lambda x: x[1], reverse=True):
print(f" - {name}: {count} attributes")
return candidates
def create_lazy_module(self, module_name: str):
"""특정 모듈을 lazy import로 전환"""
self.lazy_modules[module_name] = LazyImporter(module_name)
return self.lazy_modules[module_name]
# 사용 예시
optimizer = StartupOptimizer()
# 시작 시간 측정
start = time.time()
# 필요한 모듈만 즉시 로드
import sys
import os
# 무거운 모듈은 lazy import
pd = optimizer.create_lazy_module('pandas')
np = optimizer.create_lazy_module('numpy')
startup_time = time.time() - start
print(f"Startup time: {startup_time:.3f}초")
# 나중에 실제 사용 시 로드
# df = pd.DataFrame(...)
고급 Import 기법
Meta Path Finder 커스터마이징
Import 메커니즘을 확장하여 커스텀 로딩 로직 구현:
import sys
import importlib.abc
import importlib.machinery
class CustomFinder(importlib.abc.MetaPathFinder):
"""커스텀 import finder"""
def find_spec(self, fullname, path, target=None):
if fullname.startswith('custom_'):
# 커스텀 prefix를 가진 모듈만 처리
print(f"CustomFinder: {fullname} 찾는 중...")
# 실제 파일 경로 반환
return importlib.machinery.ModuleSpec(
fullname,
CustomLoader(),
origin='custom'
)
return None
class CustomLoader(importlib.abc.Loader):
"""커스텀 import loader"""
def create_module(self, spec):
return None # 기본 모듈 생성 사용
def exec_module(self, module):
# 모듈 코드 실행
print(f"CustomLoader: {module.__name__} 로딩 중...")
module.custom_attribute = "This is a custom module"
# Finder 등록
sys.meta_path.insert(0, CustomFinder())
# 사용
import custom_module # CustomFinder가 처리
print(custom_module.custom_attribute)
Import Hook으로 자동 Reload
개발 중 코드 변경 시 자동으로 모듈 reload:
import sys
import importlib
from importlib.abc import MetaPathFinder
from importlib.machinery import ModuleSpec
class AutoReloadFinder(MetaPathFinder):
"""변경 감지 시 자동 reload하는 finder"""
def __init__(self):
self.mtimes = {}
def find_spec(self, fullname, path, target=None):
# 기본 finder로 모듈 찾기
spec = None
for finder in sys.meta_path[1:]: # 자신은 제외
if hasattr(finder, 'find_spec'):
spec = finder.find_spec(fullname, path, target)
if spec:
break
if spec and spec.origin:
# 파일 수정 시간 확인
import os
mtime = os.path.getmtime(spec.origin)
if fullname in self.mtimes:
if mtime > self.mtimes[fullname]:
print(f"Reloading {fullname}...")
if fullname in sys.modules:
importlib.reload(sys.modules[fullname])
self.mtimes[fullname] = mtime
return spec
# 개발 모드에서만 활성화
if __debug__:
sys.meta_path.insert(0, AutoReloadFinder())
실습: 모듈 로더 커스터마이징
실습 1: JSON 설정 파일 기반 모듈 로더
JSON 파일에서 설정을 읽어와 모듈을 동적으로 생성하는 로더를 만들어봅시다.
import importlib.abc
import importlib.machinery
import sys
import json
from pathlib import Path
class JSONConfigLoader(importlib.abc.Loader):
"""JSON 설정 파일을 Python 모듈로 로드하는 커스텀 로더"""
def __init__(self, fullname, path):
self.fullname = fullname
self.path = path
def create_module(self, spec):
"""모듈 객체 생성"""
return None # 기본 모듈 생성 사용
def exec_module(self, module):
"""모듈 실행 - JSON 파일을 읽어 모듈 속성으로 설정"""
with open(self.path, 'r', encoding='utf-8') as f:
config_data = json.load(f)
# JSON 데이터를 모듈 속성으로 추가
for key, value in config_data.items():
setattr(module, key, value)
# 메타데이터 추가
module.__file__ = self.path
module.__loader__ = self
class JSONConfigFinder(importlib.abc.MetaPathFinder):
"""JSON 설정 파일을 찾는 커스텀 finder"""
def __init__(self, search_paths):
self.search_paths = search_paths
def find_spec(self, fullname, path, target=None):
"""모듈 검색"""
# 'config.'로 시작하는 모듈만 처리
if not fullname.startswith('config.'):
return None
# 실제 파일명 생성 (config.database -> database.json)
module_name = fullname.split('.')[-1]
json_filename = f"{module_name}.json"
# 검색 경로에서 파일 찾기
for search_path in self.search_paths:
json_path = Path(search_path) / json_filename
if json_path.exists():
loader = JSONConfigLoader(fullname, str(json_path))
return importlib.machinery.ModuleSpec(
fullname,
loader,
origin=str(json_path)
)
return None
# 실습: JSON 파일 생성 및 사용
if __name__ == '__main__':
# 1. 설정 파일 생성
config_dir = Path('./config_files')
config_dir.mkdir(exist_ok=True)
database_config = {
'host': 'localhost',
'port': 5432,
'database': 'myapp',
'username': 'admin',
'connection_pool_size': 10
}
with open(config_dir / 'database.json', 'w') as f:
json.dump(database_config, f, indent=2)
# 2. Finder 등록
finder = JSONConfigFinder(['./config_files'])
sys.meta_path.insert(0, finder)
# 3. JSON 파일을 모듈처럼 import
import config.database
print(f"Database Host: {config.database.host}")
print(f"Database Port: {config.database.port}")
print(f"Connection Pool: {config.database.connection_pool_size}")
# 4. 모듈 정보 확인
print(f"\nModule name: {config.database.__name__}")
print(f"Module file: {config.database.__file__}")
실습 2: 암호화된 모듈 로더
암호화된 Python 소스 코드를 복호화하여 로드하는 보안 로더를 구현합니다.
import importlib.abc
import importlib.machinery
import sys
from pathlib import Path
import base64
class EncryptedModuleLoader(importlib.abc.SourceLoader):
"""암호화된 Python 파일을 로드하는 커스텀 로더"""
def __init__(self, fullname, path, encryption_key):
self.fullname = fullname
self.path = path
self.encryption_key = encryption_key
def get_filename(self, fullname):
"""모듈 파일 경로 반환"""
return self.path
def get_data(self, path):
"""파일 데이터 읽기"""
with open(path, 'rb') as f:
return f.read()
def exec_module(self, module):
"""암호화된 모듈 실행"""
# 암호화된 데이터 읽기
encrypted_data = self.get_data(self.path)
# 복호화 (간단한 XOR 예시, 실제로는 더 강력한 암호화 사용)
decrypted_code = self._decrypt(encrypted_data)
# 복호화된 코드 실행
code = compile(decrypted_code, self.path, 'exec')
module.__file__ = self.path
module.__loader__ = self
exec(code, module.__dict__)
def _decrypt(self, data):
"""XOR 기반 간단한 복호화"""
key_bytes = self.encryption_key.encode('utf-8')
decrypted = bytearray()
for i, byte in enumerate(data):
decrypted.append(byte ^ key_bytes[i % len(key_bytes)])
return decrypted.decode('utf-8')
def _encrypt(self, source_code):
"""암호화 (유틸리티 메서드)"""
key_bytes = self.encryption_key.encode('utf-8')
encrypted = bytearray()
for i, char in enumerate(source_code.encode('utf-8')):
encrypted.append(char ^ key_bytes[i % len(key_bytes)])
return bytes(encrypted)
class EncryptedModuleFinder(importlib.abc.MetaPathFinder):
"""암호화된 모듈을 찾는 커스텀 finder"""
def __init__(self, search_path, encryption_key):
self.search_path = Path(search_path)
self.encryption_key = encryption_key
def find_spec(self, fullname, path, target=None):
"""암호화된 모듈 검색"""
if not fullname.startswith('secure_'):
return None
# secure_mymodule -> mymodule.pye (encrypted python)
module_name = fullname.replace('secure_', '')
encrypted_file = self.search_path / f"{module_name}.pye"
if encrypted_file.exists():
loader = EncryptedModuleLoader(
fullname,
str(encrypted_file),
self.encryption_key
)
return importlib.machinery.ModuleSpec(
fullname,
loader,
origin=str(encrypted_file)
)
return None
# 실습: 암호화된 모듈 생성 및 로드
if __name__ == '__main__':
# 1. 암호화할 소스 코드
secret_code = """
SECRET_API_KEY = "super-secret-key-12345"
SECRET_TOKEN = "confidential-token-xyz"
def get_secret():
return f"API Key: {SECRET_API_KEY}"
class SecureConfig:
def __init__(self):
self.api_key = SECRET_API_KEY
self.token = SECRET_TOKEN
"""
# 2. 암호화 및 저장
secure_dir = Path('./secure_modules')
secure_dir.mkdir(exist_ok=True)
encryption_key = "my-encryption-key"
loader_temp = EncryptedModuleLoader('temp', 'temp', encryption_key)
encrypted_data = loader_temp._encrypt(secret_code)
with open(secure_dir / 'secrets.pye', 'wb') as f:
f.write(encrypted_data)
print("암호화된 파일 생성 완료")
# 3. Finder 등록
finder = EncryptedModuleFinder('./secure_modules', encryption_key)
sys.meta_path.insert(0, finder)
# 4. 암호화된 모듈 import
import secure_secrets
print(f"\nSecret: {secure_secrets.get_secret()}")
config = secure_secrets.SecureConfig()
print(f"Token: {config.token}")
실습 3: HTTP 기반 원격 모듈 로더
HTTP를 통해 원격 서버에서 모듈을 동적으로 로드하는 로더를 구현합니다.
import importlib.abc
import importlib.machinery
import sys
import urllib.request
import hashlib
from pathlib import Path
class RemoteModuleLoader(importlib.abc.SourceLoader):
"""HTTP로 원격 모듈을 로드하는 커스텀 로더"""
def __init__(self, fullname, url, cache_dir):
self.fullname = fullname
self.url = url
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
def get_filename(self, fullname):
"""캐시된 파일 경로"""
cache_file = self._get_cache_path()
return str(cache_file)
def get_data(self, path):
"""데이터 읽기 (캐시 또는 다운로드)"""
cache_file = self._get_cache_path()
# 캐시 확인
if cache_file.exists():
print(f"Loading from cache: {cache_file}")
with open(cache_file, 'rb') as f:
return f.read()
# 원격에서 다운로드
print(f"Downloading from: {self.url}")
with urllib.request.urlopen(self.url) as response:
data = response.read()
# 캐시 저장
with open(cache_file, 'wb') as f:
f.write(data)
return data
def exec_module(self, module):
"""모듈 실행"""
source_bytes = self.get_data(self.get_filename(self.fullname))
source_code = source_bytes.decode('utf-8')
code = compile(source_code, self.url, 'exec')
module.__file__ = self.url
module.__loader__ = self
module.__cached__ = str(self._get_cache_path())
exec(code, module.__dict__)
def _get_cache_path(self):
"""캐시 파일 경로 생성"""
# URL을 해시하여 파일명 생성
url_hash = hashlib.md5(self.url.encode()).hexdigest()
return self.cache_dir / f"{self.fullname}_{url_hash}.py"
class RemoteModuleFinder(importlib.abc.MetaPathFinder):
"""원격 모듈을 찾는 커스텀 finder"""
def __init__(self, base_url, cache_dir='./remote_cache'):
self.base_url = base_url.rstrip('/')
self.cache_dir = cache_dir
def find_spec(self, fullname, path, target=None):
"""원격 모듈 검색"""
if not fullname.startswith('remote_'):
return None
# remote_utils -> utils.py
module_name = fullname.replace('remote_', '')
url = f"{self.base_url}/{module_name}.py"
loader = RemoteModuleLoader(fullname, url, self.cache_dir)
return importlib.machinery.ModuleSpec(
fullname,
loader,
origin=url
)
# 실습: 로컬 HTTP 서버에서 모듈 로드
if __name__ == '__main__':
# 1. 테스트용 모듈 파일 생성 (실제로는 원격 서버에 배치)
remote_modules_dir = Path('./remote_modules_server')
remote_modules_dir.mkdir(exist_ok=True)
utils_code = """
def format_number(n):
return f"{n:,}"
def calculate_tax(amount, rate=0.1):
return amount * rate
class MathUtils:
@staticmethod
def fibonacci(n):
if n <= 1:
return n
return MathUtils.fibonacci(n-1) + MathUtils.fibonacci(n-2)
"""
with open(remote_modules_dir / 'utils.py', 'w') as f:
f.write(utils_code)
# 2. 간단한 HTTP 서버 시뮬레이션 (file:// 프로토콜 사용)
import os
base_url = f"file://{os.path.abspath(remote_modules_dir)}"
# 3. Finder 등록
finder = RemoteModuleFinder(base_url)
sys.meta_path.insert(0, finder)
# 4. 원격 모듈 import
import remote_utils
print(f"\nFormatted number: {remote_utils.format_number(1234567)}")
print(f"Tax: ${remote_utils.calculate_tax(100)}")
print(f"Fibonacci(10): {remote_utils.MathUtils.fibonacci(10)}")
# 5. 캐시 확인
print(f"\nCached file: {remote_utils.__cached__}")
실습 4: 버전 관리가 있는 모듈 로더
여러 버전의 모듈을 동시에 로드하고 사용할 수 있는 버전 관리 시스템을 구현합니다.
import importlib.abc
import importlib.machinery
import sys
from pathlib import Path
from typing import Dict
class VersionedModuleLoader(importlib.abc.SourceLoader):
"""버전별 모듈을 로드하는 로더"""
def __init__(self, fullname, version, path):
self.fullname = fullname
self.version = version
self.path = path
def get_filename(self, fullname):
return self.path
def get_data(self, path):
with open(path, 'rb') as f:
return f.read()
def exec_module(self, module):
source_bytes = self.get_data(self.path)
source_code = source_bytes.decode('utf-8')
code = compile(source_code, self.path, 'exec')
module.__file__ = self.path
module.__loader__ = self
module.__version__ = self.version
exec(code, module.__dict__)
class VersionedModuleFinder(importlib.abc.MetaPathFinder):
"""버전별 모듈을 찾는 finder"""
def __init__(self, versions_root):
self.versions_root = Path(versions_root)
def find_spec(self, fullname, path, target=None):
"""
모듈명 형식: mylib_v1_2_3
실제 파일: versions/mylib/1.2.3/mylib.py
"""
parts = fullname.split('_v')
if len(parts) != 2:
return None
module_name = parts[0]
version = parts[1].replace('_', '.')
# 버전별 디렉토리에서 모듈 찾기
module_path = (
self.versions_root /
module_name /
version /
f"{module_name}.py"
)
if module_path.exists():
loader = VersionedModuleLoader(fullname, version, str(module_path))
return importlib.machinery.ModuleSpec(
fullname,
loader,
origin=str(module_path)
)
return None
# 실습: 여러 버전의 모듈 생성 및 로드
if __name__ == '__main__':
# 1. 버전별 모듈 생성
versions_dir = Path('./module_versions')
# 버전 1.0.0
v1_dir = versions_dir / 'calculator' / '1.0.0'
v1_dir.mkdir(parents=True, exist_ok=True)
with open(v1_dir / 'calculator.py', 'w') as f:
f.write("""
def add(a, b):
return a + b
VERSION_INFO = "Basic calculator v1.0.0"
""")
# 버전 2.0.0
v2_dir = versions_dir / 'calculator' / '2.0.0'
v2_dir.mkdir(parents=True, exist_ok=True)
with open(v2_dir / 'calculator.py', 'w') as f:
f.write("""
def add(a, b):
return a + b
def multiply(a, b):
return a * b
def power(a, b):
return a ** b
VERSION_INFO = "Enhanced calculator v2.0.0"
""")
# 2. Finder 등록
finder = VersionedModuleFinder('./module_versions')
sys.meta_path.insert(0, finder)
# 3. 여러 버전 동시 사용
import calculator_v1_0_0 as calc_v1
import calculator_v2_0_0 as calc_v2
print(f"V1 Info: {calc_v1.VERSION_INFO}")
print(f"V1 add(5, 3): {calc_v1.add(5, 3)}")
print(f"\nV2 Info: {calc_v2.VERSION_INFO}")
print(f"V2 add(5, 3): {calc_v2.add(5, 3)}")
print(f"V2 multiply(5, 3): {calc_v2.multiply(5, 3)}")
print(f"V2 power(5, 3): {calc_v2.power(5, 3)}")
# 4. 버전 정보 확인
print(f"\nV1 version: {calc_v1.__version__}")
print(f"V2 version: {calc_v2.__version__}")
실습 요약 및 응용
위 실습들을 통해 배운 핵심 개념:
-
JSONConfigLoader: 비-Python 파일을 모듈로 변환
- 응용: YAML, TOML, XML 설정 파일 로더
- 실무: 설정 관리 시스템 구축
-
EncryptedModuleLoader: 소스 코드 보안
- 응용: 라이선스 관리, 지적 재산 보호
- 실무: 상용 Python 애플리케이션 배포
-
RemoteModuleLoader: 원격 코드 실행
- 응용: 플러그인 마켓플레이스, 동적 업데이트
- 실무: 클라우드 기반 모듈 배포
-
VersionedModuleLoader: 버전 관리
- 응용: A/B 테스트, 점진적 마이그레이션
- 실무: 레거시 시스템과 신규 시스템 공존
종합 연습 과제:
위 4가지 로더를 결합하여 다음 기능을 가진 통합 모듈 관리 시스템을 만들어보세요:
- 원격 서버에서 암호화된 모듈을 다운로드
- 버전별로 캐싱
- JSON 설정 파일로 로드 정책 관리
- 로그 및 보안 감사 기능
실전 문제 해결
순환 Import 문제
문제 상황:
# module_a.py
from module_b import ClassB
class ClassA:
def use_b(self):
return ClassB()
# module_b.py
from module_a import ClassA # 순환 import!
class ClassB:
def use_a(self):
return ClassA()
해결 방법 1: 함수 내부 import
# module_b.py
class ClassB:
def use_a(self):
from module_a import ClassA # 지연 import
return ClassA()
해결 방법 2: 구조 재설계
# base.py
class ClassA:
pass
class ClassB:
pass
# module_a.py
from base import ClassA, ClassB
# module_b.py
from base import ClassA, ClassB
조건부 Import
플랫폼이나 Python 버전에 따른 import:
import sys
# Python 버전별 import
if sys.version_info >= (3, 11):
from typing import Self
else:
from typing_extensions import Self
# 플랫폼별 import
if sys.platform == 'win32':
import msvcrt
elif sys.platform == 'darwin':
import readline
else:
import readline
# Optional dependency
try:
import pandas as pd
HAS_PANDAS = True
except ImportError:
HAS_PANDAS = False
def analyze_data(data):
if not HAS_PANDAS:
raise RuntimeError("pandas가 필요합니다: pip install pandas")
return pd.DataFrame(data)
핵심 포인트
- Import는 단순한 문법이 아니라 복잡한 시스템: 5단계 프로세스를 거침
- sys.modules 캐싱: 모듈은 한 번만 로드되고 캐시됨
- sys.path: 모듈 검색 경로를 동적으로 조정 가능
- importlib: 동적 import와 커스텀 로딩 로직 구현 가능
- Lazy import: 시작 시간을 단축하는 최적화 기법
- Meta Path Finder: Import 메커니즘을 확장하여 플러그인 시스템 구현 가능
- 순환 import: 함수 내부 import 또는 구조 재설계로 해결
결론
Python의 import 시스템은 단순해 보이지만 내부적으로는 매우 정교한 메커니즘을 가지고 있습니다. 이 글에서는 import의 기초부터 고급 기법까지 다루었습니다.
주요 학습 내용:
- Import 기초: import vs from import, 절대/상대 경로,
__init__.py - 내부 동작: sys.path, sys.modules 캐싱, 5단계 import 프로세스
- 고급 기법: importlib 활용, lazy import, meta path finder
- 실전 해결: 순환 import, 플러그인 시스템, 자동 reload
이러한 지식을 바탕으로:
- 복잡한 프로젝트 구조를 효율적으로 관리할 수 있습니다
- Import 관련 오류를 빠르게 해결할 수 있습니다
- 플러그인 아키텍처를 설계하고 구현할 수 있습니다
- 시작 시간을 최적화하여 애플리케이션 성능을 향상시킬 수 있습니다
이전 학습
이 글을 더 잘 이해하기 위해 먼저 읽어보세요:
- Python Bytecode ← 이전 추천
- Import된 모듈이 어떻게 바이트코드로 컴파일되고 실행되는지 이해하세요
다음 학습
Python-Essential 시리즈:
- 다음: Python exception internals 로깅 및 Stack trace 분석 - 예외 처리 내부 구조와 디버깅 기법
관련 주제:
- Python 패키지 구조 설계 패턴
- Python Metaclass와 클래스 생성 메커니즘
- Python 플러그인 아키텍처 설계
- Python 디버깅 고급 기법