패키지 구조 — __init__.py, 상대/절대 임포트, namespace packages
**패키지(package)**는 여러 모듈을 하나의 디렉터리로 묶은 것입니다. 대규모 프로젝트에서 코드를 논리적으로 구성하고, 재사용 가능한 라이브러리를 배포하는 기반이 됩니다.
패키지란: 디렉터리 + __init__.py
전통적인 패키지는 디렉터리 안에 __init__.py 파일이 있어야 합니다.
mypackage/
__init__.py
core.py
utils.py
models/
__init__.py
user.py
product.py
# mypackage를 임포트하면 __init__.py가 실행됨
import mypackage
from mypackage import core
from mypackage.models import user
__init__.py의 역할
__init__.py는 패키지를 초기화하고 공개 API를 정의합니다.
빈 __init__.py
단순히 디렉터리를 패키지로 인식시킵니다.
# mypackage/__init__.py (빈 파일도 OK)
공개 API 정의
# mypackage/__init__.py
"""mypackage — 유용한 유틸리티 모음"""
from .core import MainClass, helper_func
from .utils import format_date, parse_config
__version__ = "1.2.0"
__author__ = "홍길동"
__all__ = ["MainClass", "helper_func", "format_date", "parse_config"]
이렇게 하면 사용자는 내부 구조를 알 필요 없이 최상위에서 임포트할 수 있습니다:
from mypackage import MainClass, helper_func # 내부 경로 몰라도 OK
지연 임포트로 성능 최적화
# mypackage/__init__.py
def get_heavy_module():
"""무거운 모듈은 처음 사용할 때만 로드"""
from . import heavy_processing
return heavy_processing
상대 임포트: from . import X
패키지 내부에서 같은 패키지의 다른 모듈을 참조할 때 사용합니다.
# mypackage/core.py
# 절대 임포트
from mypackage.utils import helper # 패키지명 명시 필요
# 상대 임포트
from .utils import helper # . = 현재 패키지
from ..common import shared_func # .. = 상위 패키지
from . import models # 서브패키지 임포트
project/
mypackage/
__init__.py
core.py # from .utils import helper
utils.py
models/
__init__.py
user.py # from ..utils import helper (상위로 올라감)
# mypackage/models/user.py
from ..utils import format_date # mypackage.utils.format_date
from ..core import BaseModel # mypackage.core.BaseModel
from . import product # mypackage.models.product
주의: 상대 임포트는 패키지 내부에서만 사용 가능합니다. 최상위 스크립트에서는 사용할 수 없습니다.
절대 임포트 vs 상대 임포트
| 구분 | 절대 임포트 | 상대 임포트 |
|---|---|---|
| 형식 | from mypackage.utils import func | from .utils import func |
| 명확성 | 어디서든 명확한 경로 | 현재 위치 기준 상대 경로 |
| 리팩터링 | 패키지명 변경 시 전체 수정 | 패키지명 변경에 독립적 |
| 권장 상황 | 공개 API, 외부에서 사용 | 패키지 내부 모듈 간 참조 |
PEP 8은 절대 임포트를 기본으로 권장하고, 내부 참조에서는 상대 임포트도 허용합니다.
# 권장: 명확한 절대 임포트
from mypackage.models.user import User
# 패키지 내부에서는 상대 임포트도 좋음
from .models.user import User
Namespace Packages (PEP 420): __init__.py 없는 패키지
Python 3.3+에서 __init__.py 없이도 패키지를 구성할 수 있습니다. 주로 대규모 프로젝트에서 여러 디렉터리에 걸쳐 하나의 네임스페이스를 공유할 때 사용합니다.
# 서로 다른 위치에 있지만 같은 네임스페이스를 공유
/project_a/myorg/auth/login.py
/project_b/myorg/payments/checkout.py
# __init__.py 없이도 임포트 가능
from myorg.auth.login import authenticate
from myorg.payments.checkout import process
import sys
# 두 경로를 sys.path에 추가
sys.path.extend(["/project_a", "/project_b"])
# 네임스페이스 패키지로 통합 접근
from myorg.auth import login
from myorg.payments import checkout
실제로는 플러그인 아키텍처나 모노레포 환경에서 활용됩니다.
패키지 구조 설계 패턴 (src layout)
현대적인 Python 프로젝트의 권장 구조인 src layout입니다.
my-project/
src/
mypackage/
__init__.py
core/
__init__.py
engine.py
parser.py
utils/
__init__.py
helpers.py
validators.py
cli/
__init__.py
main.py
tests/
__init__.py
test_core.py
test_utils.py
pyproject.toml
README.md
src layout의 장점:
- 설치되지 않은 패키지를 실수로 임포트하는 것을 방지
- 테스트가 설치된 패키지를 테스트하도록 강제
editable install(pip install -e .)과 잘 호환
# pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.backends.legacy:build"
[project]
name = "mypackage"
version = "1.0.0"
[tool.setuptools.packages.find]
where = ["src"] # src 디렉터리에서 패키지 찾기
실전 예제: 플러그인 패키지 구조
plugin_system/
src/
core/
__init__.py
registry.py
base_plugin.py
plugins/
__init__.py
csv_plugin.py
json_plugin.py
xml_plugin.py
tests/
test_registry.py
# src/core/base_plugin.py
from abc import ABC, abstractmethod
from typing import Any
class BasePlugin(ABC):
"""모든 플러그인의 기반 클래스"""
name: str = ""
@abstractmethod
def process(self, data: Any) -> Any:
"""데이터 처리"""
...
@abstractmethod
def validate(self, data: Any) -> bool:
"""입력 검증"""
...
# src/core/registry.py
from __future__ import annotations
from typing import Type
from .base_plugin import BasePlugin
__all__ = ["PluginRegistry"]
class PluginRegistry:
"""플러그인 등록 및 관리"""
_plugins: dict[str, Type[BasePlugin]] = {}
@classmethod
def register(cls, plugin_cls: Type[BasePlugin]) -> Type[BasePlugin]:
"""데코레이터로 플러그인 등록"""
cls._plugins[plugin_cls.name] = plugin_cls
return plugin_cls
@classmethod
def get(cls, name: str) -> Type[BasePlugin]:
if name not in cls._plugins:
raise KeyError(f"플러그인 '{name}'이 등록되지 않았습니다.")
return cls._plugins[name]
@classmethod
def list_all(cls) -> list[str]:
return list(cls._plugins.keys())
# src/core/__init__.py
from .registry import PluginRegistry
from .base_plugin import BasePlugin
__all__ = ["PluginRegistry", "BasePlugin"]
# src/plugins/csv_plugin.py
from ..core import PluginRegistry, BasePlugin
import csv
import io
@PluginRegistry.register
class CsvPlugin(BasePlugin):
name = "csv"
def process(self, data: list[dict]) -> str:
output = io.StringIO()
if data:
writer = csv.DictWriter(output, fieldnames=data[0].keys())
writer.writeheader()
writer.writerows(data)
return output.getvalue()
def validate(self, data: list[dict]) -> bool:
return isinstance(data, list) and all(isinstance(row, dict) for row in data)
고수 팁
팁 1: __init__.py에서 버전 관리
# mypackage/__init__.py
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("mypackage")
except PackageNotFoundError:
__version__ = "unknown" # 개발 환경에서 설치되지 않은 경우
팁 2: 순환 임포트 방지를 위한 TYPE_CHECKING 패턴
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from .models import User # 런타임에는 임포트되지 않음
def get_user() -> "User": # 타입 힌트만 사용
from .models import User # 실제 사용 시점에 임포트
return User()
팁 3: __all__과 __init__.py로 깔끔한 공개 API 구성
# mypackage/__init__.py
from .core import Engine
from .utils import format_output
from .models import User, Product
# 사용자에게 노출할 이름만 명시
__all__ = ["Engine", "format_output", "User", "Product"]
팁 4: 패키지 데이터 파일 접근
from importlib.resources import files
# 패키지 내 데이터 파일 읽기 (Python 3.9+)
config_text = files("mypackage").joinpath("data/default_config.toml").read_text()
template = files("mypackage.templates").joinpath("base.html").read_text()