본문으로 건너뛰기
Advertisement

패키지 구조 — __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 funcfrom .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()
Advertisement