본문으로 건너뛰기

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