python - 기본적인 파이썬 스타일 가이드와 import, init 그리고 모듈화

정현우·2025년 11월 12일
post-thumbnail

[ 글의 목적: 파이썬 스타일 가이드와 import & init, 기본적인 "모듈화" 팁 기록, 사실은 교육자료로 쓰려고 작성함 ]

파이썬 스타일 가이드와 import, init 그리고 모듈화

python 의 자유도 덕분에 진입 장벽이 낮기에 초기엔 코드 스타일이 굉장히 다이나믹하다.. (정말 막 써도 실행이 되니까..!) 하지만 코드는 작성하는 시간보다 읽는 시간이 길다. 그리고 우리는 항상 AI를 포함한 '누군가' 와 같이 일한다. 일관된 스타일은 가독성을 높이고 유지보수 비용을 낮춘다. 특히 LLM의 시대에는 이 규칙, 일관성이 더 중요해지고 있고 Zen of Python 에서도 “Readability counts” 라고 강조한다.

이 글에서는 PEP 8 (Python 공식 스타일 가이드)과 Google Python Style Guide (대규모 협업을 위한 권장 규칙), PEP 484 (타입 힌트 표준), 그리고 Python import 시스템 (정규 패키지 vs 네임스페이스 패키지, 절대 vs 상대 import)에 기반하여 글을 작성해보고자 한다.

1. 스타일 가이드

1) PEP8 핵심

  1. 네이밍(Naming): 함수·변수는 소문자 스네이크케이스(snake_case), 클래스는 단어마다 대문자(CapWords/PascalCase) 사용, 상수는 대문자+snake_case로 작성한다. (예: MAX_COUNT)

  2. 들여쓰기(Indentation): 스페이스 4칸을 사용한다. 탭 혼용은 금지되고, 기존 코드가 탭을 쓰지 않는 한 새 코드에는 탭을 사용하지 않는다.

  3. 라인 길이(Line Length): 한 줄에 79자 제한을 권장한다. Docstring이나 주석은 72자 이내로 줄 바꿈한다. (팀 합의로 88자나 100자 등으로 늘릴 수도 있으나, 일단 팀 내부에서는 일관성 있게 유지해야 한다. PEP 8에 따르면 팀 프로젝트의 경우 99자까지 허용할 수 있지만 docstring/comment는 여전히 72자 권장이다.)

    • 정말 왈가왈부가 많은 부분이다 ㅎㅎ. 개인적으로 88자~100자 사이를 더 선호한다. 특히 logging 처리나 문자열처리에 이 길이제한이 너무 과하다고 많이 느낀다.
  4. 공백/여백(Whitespace): 불필요한 공백을 지양한다. 예를 들어 쉼표나 콜론 앞에는 공백을 넣지 않고 뒤에만 넣는다. 키워드 인자 기본값 설정 시에도 = 양옆에 공백을 넣지 않는 것이 규칙이다.

    • 타입 힌트가 있을 때는 예외: def func(arg: int = 0) -> None: 처럼 기본값 설정의 = 앞뒤에 공백을 넣는 것이 권장된다.
    • 추가로 주석은 스페이스 2칸을 띄우고 # 주석 을 붙인다.
  5. import 순서: PEP 8 권장 순서는 ① 표준 라이브러리 ② 서드파티 ③ 로컬 모듈 순으로, 각 블록마다 알파벳 순서를 유지한다. 각 그룹 사이에는 빈 줄을 넣어 구분한다.

    • 완전 사견이고 취향일 수 있는데 여기서 python에 얼마나 익숙한지 차이가 보인다고 느껴진다. 왜냐면 애초에 처음 python을 사용하면 표준 라이브러리, 서드파티를 잘 구분못하기에...
  6. Docstring: 공개 API에는 Docstring을 작성하고, 세부 규칙은 PEP 257을 따른다. 요약은 한 줄로, 이어서 공백 줄, 그리고 자세한 설명 순으로 적는다. 따옴표 3개(""")로 감싸고, 한 줄 요약은 마침표로 끝내도록 한다.

  7. 사람 대신 자동 포매터(formatter)린터(linter) 가 스타일을 강제하도록 해보자! uv & ruff 설치부터 project initializing 참조!

개인적으로 어떤 언어든 익숙해지기전부터 save할때 auto-formatting 은 절대 비추천한다. 린팅과 포멧팅을 수기로 돌리는게 이런 rule에 대해 익숙해지기 쉽다고 생각한다.

2) Google Python Style Guide 포인트

https://google.github.io/styleguide/pyguide.html 참조. 많아보이지만 생각보다 복잡하지 않아서 한 10분이면 한 숨에 다 읽을 수 있다.

구글 스타일가이드에서 가장 핵심포인트는 “이렇게 해도 된다”보다 “이렇게 하지 말라”는 금지를 분명하게 한다는 점이다.

  1. PEP 8 기반 + 명확한 가이드: Google 스타일 가이드는 기본적으로 PEP 8을 따르면서, 각 항목마다 팀 컨벤션으로서 “Yes/No”와 권장/비권장/결론을 명시해준다. 이를 통해 스타일 논쟁을 줄이고 모두가 동일한 결정을 따르도록 유도한다.

  2. Docstring과 타입: Google 스타일 가이드는 Docstring에 Args/Returns/Raises 섹션을 명확히 적는 형식을 제시하며, 함수와 메소드에 타입 힌트 사용을 적극 권장한다. 예를 들어, 인자와 리턴 타입을 명시하고, 예외 발생 가능성을 Docstring에 기술하도록 권고한다. 또한 새로운 공개 API를 작성할 때는 반드시 타입 어노테이션을 포함하고, pytype 등의 정적 검사기를 CI에 적용할 것을 요구한다. (PEP 484의 선택적 타입 체크 개념을 적극 도입하는 추세와 맥을 같이 한다.)

def analyze_sales_data(
    transactions: list[dict[str, str | int | float]],
    min_amount: float = 0.0,
    category_filter: str | None = None
) -> dict[str, float]:
    """거래 데이터를 분석하여 카테고리별 총액을 계산합니다.

    Args:
        transactions: 거래 정보를 담은 딕셔너리 리스트.
            각 딕셔너리는 'category', 'amount', 'date' 키를 포함해야 함
        min_amount: 집계에 포함할 최소 거래 금액 (기본값: 0.0)
        category_filter: 특정 카테고리만 필터링 (None이면 모든 카테고리 포함)

    Returns:
        카테고리명을 키로, 총 거래액을 값으로 하는 딕셔너리

    Raises:
        ValueError: min_amount가 음수인 경우
        KeyError: 필수 키('category', 'amount')가 누락된 경우
    """
	
    if min_amount < 0:
        raise ValueError("min_amount는 0 이상이어야 합니다")

    result: dict[str, float] = {}

    for transaction in transactions:
        # 필수 키 검증
        if 'category' not in transaction or 'amount' not in transaction:
            raise KeyError("거래 데이터에 'category'와 'amount' 키가 필요합니다")

        category = transaction['category']
        amount = transaction['amount']

        # 타입 검증 및 변환
        if not isinstance(category, str):
            continue
        if not isinstance(amount, (int, float)):
            continue

        # 필터링 조건 확인
        if category_filter and category != category_filter:
            continue
        if float(amount) < min_amount:
            continue

        # 집계
        result[category] = result.get(category, 0.0) + float(amount)

    return result
  1. 명확한 금지 규정: 예를 들어 mutable 한 type 기본 인자 사용 금지, 예외 사용 가이드, global 변수 사용 지양 등 실무에서 발생하는 문제 상황에 대한 규칙이 제시되어 있어, 팀원들이 의도를 파악하기 쉽다.

그러니 당장 인하우스에서 직접 A to Z 코드 스타일 정의하기 어렵다면, 그냥 PEP8 + Google Python Style Guide 를 그대로 녹이는 건 어떨까?

3) “mypy 같은 static type checker 없어도” 시그니처 힌트 습관화!

  1. PEP 484(Type Hints)은 파이썬에 표준화된 타입 표기법을 도입한 제안으로, 3.5 버전부터 적용되었다. 이는 타입 주석을 활용해 정적 분석과 리팩토링, IDE 지원을 개선하려는 목적이었다. 예를 들어 함수 인자와 반환값에 타입을 써두면, IDE가 자동완성이나 오류 검출을 더 잘해줄 수 있다.

  2. 동적 타입 언어에 선택적 적용: 타입 힌트는 강제 사항이 아니며, 런타임에 아무 검사도 하지 않는다. 파이썬 인터프리터는 타입 어노테이션을 무시하고 (__annotations__ 에 저장만 함) 실행에 영향을 주지 않는다. 대신 mypy 같은 별도 오프라인 타입 체크 도구로 검사하도록 설계되었다. 다시 말해 파이썬은 여전히 동적 타이핑 언어이고, 타입 힌트는 권장사항일 뿐이다. (PEP 484에도 “타입 힌트를 절대 의무화하지 않을 것”이라고 명시되어 있다.)

  3. 그럼에도 불구하고, 타입 힌트는 점진적으로 많은 프로젝트에서 채택되고 있다. 표준 라이브러리의 typing 모듈이 다양한 타입 힌트 표현을 제공하고, Python 3.9+에서는 list[int] 같이 내장 컬렉션에 제네릭 표기를 지원하며, 3.10부터는 X | Y 형태의 유니언 표기법도 추가되는 등 타입 힌트 문법이 꾸준히 발전 중이다.

    • 그러니 from typing 과 같은 import 는 이제 사실 굳이..? 필자는 그렇기에 최소 3.10 이상, 가능하다면 3.12 이상 사용을 권한다.
    • dataclass, Pydantic 에서도 이 제네릭 표기를 지원한다!

  1. 새 코드엔 타입 힌트 권장: 팀 차원에서 새로운 함수나 메소드를 작성할 때는 파라미터 타입과 리턴 타입을 꼭 명시하도록 한다. 기존 코드를 리팩토링할 때도 함수 시그니처에 타입을 추가하는 것을 권장한다. (Google Style Guide도 신규 public API에 타입 어노테이션을 반드시 추가하도록 요구한다.)

  2. 컨테이너 타입 명시: list[int], dict[str, Any] 처럼 내부 요소 타입을 제네릭으로 명시한다. Python 3.9부터 list[int] 구문을 지원하므로 별도 from typing import List를 하지 않고도 표기가 가능하다. 불특정 타입은 Any를 사용하고, 가능하면 구체적인 Protocol이나 TypeVar로 대체를 고민한다.

  3. 유니언과 옵션: X | Y 표기 (typing.Union[X, Y]의 축약)로 여러 타입을 허용할 수 있다. Optional[X]X | None으로 쓸 수 있다 (| 연산자 지원은 Python 3.10+).

  1. TypedDict/Protocol 활용: 복잡한 구조는 TypedDict(PEP 589)로 키-값 타입을 정의하거나, Protocol(PEP 544)로 인터페이스를 정의해두면 코드 이해에 도움이 된다. 이러한 고급 타입은 팀원들이 충분히 숙지한 경우에 단계적으로 도입한다.

아래는 타입 힌트를 포함한 간단한 함수 예시이다. 타입 힌트로 함수의 입력과 출력이 명확히 드러나므로, 읽는 사람이 함수 의도를 쉽게 파악할 수 있다. 또한 잘못된 타입을 넣으려고 하면 IDE나 린터가 경고해줄 수 있다.

from typing import Iterable, Sequence

def topk(items: Sequence[int], k: int) -> list[int]:
    """Return top-k largest integers from the sequence."""
    if k < 0:
        raise ValueError("k must be non-negative")
    return sorted(items, reverse=True)[:k]

def join_csv(tokens: Iterable[str]) -> str:
    """Join iterable of strings into one CSV string."""
    return ",".join(tokens)

위 코드에서 topk 함수는 정수 시퀀스를 받아 상위 k개 정수를 리스트로 반환하며, join_csv는 문자열 이터러블을 받아 콤마로 연결한 문자열을 반환한다. 각각 타입 어노테이션(Sequence[int], list[int], Iterable[str], str)을 달아두었기 때문에 함수 사용법이 명확해진다.

이제 대부분의 DTO 도 dataclass 선에서 모두 정리가 가능하다. python - 파이써닉한 dataclass 와 ENUM


2. 모듈화, import, init

1) Python import 시스템 개요

  • 모듈과 패키지: 파이썬에서 파일 하나(.py)는 곧 모듈(module)이고, 폴더는 패키지(package)로 취급된다. 모든 패키지는 모듈의 한 종류일 뿐이며, 단지 __path__ 라는 속성을 지닌 모듈이 패키지가 된다. 서브패키지는 점으로 상위 패키지와 연결된 "네임스페이스" 로 구성된다! (예: email.mime.text는 email 패키지 아래 mime 패키지의 text 모듈).

  • "네임스페이스" 라는 단어가 아직 익숙하지 않다면, 다음 2개의 링크 및 글을 추천한다. (1) 네임스페이스 설명 위키, (2) Namespaces in Python

  • 정규 패키지 vs 네임스페이스 패키지: 파이썬 3.2까지의 전통적인 패키지는 정규 패키지(regular package) 로, 해당 디렉터리에 __init__.py 파일이 있어야 패키지로 인식되고 첫 import__init__.py 가 실행되어 패키지 네임스페이스가 구성된다. 반면 파이썬 3.3부터 도입된 네임스페이스 패키지(namespace package)__init__.py 없이도 패키지로 인식되며, 하나의 논리 패키지를 여러 디렉터리에 걸쳐 분산시킬 수 있다. 즉, 동일한 패키지 이름을 가진 폴더가 여러 경로에 있어도 하나의 패키지 네임스페이스로 합쳐진다 (이 기능은 PEP 420에 정의됨).

  • 사실 __init__.py 은 구버전의 잔재로 보이지만 여전히 활용도가 높다. 파이썬 3.4부터 import 동작을 표현하는 ModuleSpec 객체 개념이 도입되었다. importlib.machinery.ModuleSpec 은 모듈을 찾고 적재할 때 필요한 메타데이터를 담고 있어, import 시스템을 더 일관되고 투명하게 만들었다. PEP 451 덕분에 finders/loaders 등의 내부 API가 단순화되고 향후 개선의 발판이 마련되었다. (이 부분은 import 훅을 직접 구현하는 경우를 제외하면 파이썬 사용자에게 내부 동작이 투명해진 정도의 의미가 있다.)

Python's Import System - Module object|Regular/Namespace Packages|Finders & Loaders|Relative imports 영상을 아주 적극적으로 추천한다.

2) init.py의 탄생 배경과 현재 위치

  • 과거의 역할: 과거 파이썬에서는 디렉터리가 패키지로 인식되기 위해 반드시 __init__.py 파일이 필요했다. 이 파일이 존재하면 해당 폴더를 패키지로 간주하여 import 할 수 있었고, import 시 파일 내부의 코드가 실행되면서 패키지 초기화가 이루어졌다. 또한 패키지 수준에서 사용될 변수나 함수를 정의하거나, 하위 모듈을 import 해서 네임스페이스에 미리 올려주는 등의 초기화 용도로 활용됐다.

  • PEP 420 이후: 파이썬 3.3부터는 __init__.py 가 없어도 패키지를 만들 수 있게 되었는데(Implicit Namespace Packages, PEP 420), 여러 분산된 패키지를 하나로 취급할 수 있는 유연성이 생겼다. 따라서 지금은 꼭 필요하지 않은 경우라면 패키지 초기화 파일을 생략할 수 있다. 예컨대 단순히 여러 작은 패키지를 모아 네임스페이스만 공유하고 싶은 경우(예: company.plugin.alpha, company.plugin.beta 등 여러 배포 패키지를 하나의 논리 패키지로 묶을 때) __init__.py 없이도 동작하게 할 수 있다.

3) 그럼에도 실무에서는 init.py가 다음 상황에서 여전히 유용하거나 필요하다.

  1. 초기화 코드가 필요한 경우: 패키지 import 시 한 번 실행되어야 하는 설정이나 검증 코드가 있다면 __init__.py 에 넣어둘 수 있다. 네임스페이스 패키지는 이러한 코드를 쓸 곳이 없다.
# mypackage/__init__.py
import sys
import logging

# 패키지 버전 정의
__version__ = "1.0.0"

# 최소 Python 버전 검증
MIN_PYTHON = (3, 8)
if sys.version_info < MIN_PYTHON:
    raise RuntimeError(
        f"This package requires Python {MIN_PYTHON[0]}.{MIN_PYTHON[1]} or higher. "
        f"You are using Python {sys.version_info.major}.{sys.version_info.minor}."
    )

# 패키지 레벨 로거 설정 (한 번만 실행)
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())

print(f"Initialized {__name__} v{__version__}")
# main.py
import mypackage  # "Initialized mypackage v1.0.0" 출력 + 버전 검증 실행

print(mypackage.__version__)  # "1.0.0"
print(mypackage.logger)  # <Logger mypackage (WARNING)>
  • 정규 패키지의 경우, 최초 import package 시에 package/__init__.py 가 실행되어 패키지의 __dict__ (네임스페이스)이 채워진다. 이때 필요한 변수를 설정하거나, 로그를 설정하는 등 초기화 작업을 수행할 수 있다. 다만, 복잡한 초기화는 지양하고 가능하면 최소한의 작업만 하는 것이 좋다 (import 시간 지연을 피하기 위해).
  1. 공개 API 재노출: 패키지 내부 구현 경로를 숨기고, 일관된 상위 API만 노출하고 싶을 때다. __init__.py 에서 하위 모듈의 클래스나 함수를 import 해서 상위 패키지 네임스페이스에 올려주면, 사용자는 깊은 경로를 신경쓰지 않고도 일관된 방법으로 사용할 수 있다.
*** 가령 디렉토리 구조가 아래와 같을 때 *** 

mylib/
  __init__.py
  internal/
    __init__.py
    database.py  -> class DatabaseConnection
    auth.py      -> class Authenticator
  utils/
    __init__.py
    validators.py -> validate_email()
# mylib/__init__.py
"""공개 API 정의 - 사용자는 내부 구조를 몰라도 됨"""

# 내부 모듈에서 핵심 클래스/함수만 가져와서 패키지 최상위 네임스페이스에 노출
from .internal.database import DatabaseConnection
from .internal.auth import Authenticator
from .utils.validators import validate_email

# from mylib import * 시 노출할 항목 명시
__all__ = ["DatabaseConnection", "Authenticator", "validate_email"]
  • __all__ 에 명시된 이름들은 from package import * 할 때 가져올 대상이고, 일반 import에는 영향 주지 않는다.

  • 이 방법은 서로 import 하는 것에 대한 이해도가 없다면! 순환 참조를 일으킬 수 있으므로 유의해야 한다. 특히 패키지 간 의존성이 복잡한 경우 __init__.py 에서 너무 많은 것을 가져오면 import 순환 문제가 생길 수 있다. 따라서 상호 의존성이 없는 경량 객체 위주로 활용하는 것이 좋다.

# 실제 사용 예시
# 사용자 코드 - 간결한 import (내부 구조 은닉)
from mylib import DatabaseConnection, Authenticator, validate_email

# 내부 경로를 알 필요 없음!
db = DatabaseConnection()
auth = Authenticator()
is_valid = validate_email("user@example.com")
  1. 패키지 리소스 접근: importlib.resources 등을 활용할 때, 패키지를 정규 패키지로 두는 편이 리소스 경로 관리가 명시적이다. 네임스페이스 패키지는 물리적 디렉터리가 여러 개일 수 있어 리소스 파일 관리가 복잡해질 수 있다. 예를 들어 files() 함수를 통해 패키지 리소스를 읽을 수 있다.
from importlib.resources import files
config_text = files("my_pkg.data").joinpath("config.toml").read_text(encoding="utf-8")
  • 위 코드는 my_pkg/data/config.toml 파일을 읽는 예시다. 과거에는 pkg_resources 등을 사용했지만, 이제 표준 라이브러리에서 안전하고 일관된 방법을 제공하므로 이를 쓰는 것이 권장된다. 이처럼 패키지 구조와 코드가 분리되어 있을 경우(data 폴더 등), 정규 패키지의 __init__.py 에 해당 서브패키지를 명시적으로 import 해두면 (필요 시) 리소스 접근에도 용이하다.

4) 절대 vs 상대 import (PEP 328)

  • 절대 경로 import 우선: 기본 원칙은 절대 경로 import를 사용하라는 것이다. 절대 import란 import my_project.utils.parser 또는 from my_project.utils import parser 처럼 최상위 패키지명부터 명시하는 방식이다.

  • 이는 모듈 출처를 명확히 보여주며, 리팩토링(모듈 이동/이름 변경) 시에도 영향 범위를 좁게 만든다. PEP 8에서도 “모든 import는 기본적으로 절대 import로 해라. 내부 모듈과 표준 라이브러리가 이름 충돌할 경우 절대 import가 모호성을 줄인다”라고 권고한다.

  • 상대 import 사용 시기: from . import submodule 같은 명시적 상대 import는 "동일 패키지 내 모듈을 참조할 때" 사용할 수 있다. 다만, PEP 8에 따르면 상대 import는 패키지 레이아웃이 복잡해서 절대 경로가 너무 장황해질 때 등 제한적인 경우에만 쓰고, 일반적으로는 지양한다. 특히 최상위 패키지 경로가 바뀔 수 있는 대형 프로젝트가 아니라면 절대 import로 충분하다. 상대 import를 남용하면 다른 개발자가 코드의 의존 관계를 파악하기 어려워질 수 있다.

  • Python 2와의 관계: PEP 328 이전엔 (Python 2 시절) 현재 패키지 기준의 암시적 import가 가능했지만, Python 3에서는 명시적 상대 import만 허용하고 기본은 절대 import로 동작한다. 요즘 코드는 모두 Python 3이므로 특별히 from __future__ import absolute_import 등을 신경 쓸 필요는 없지만, 과거 코드 일부를 가져올 경우 이런 맥락을 알아두면 도움이 된다.

absolute_import 왜 씀?

  • Python 2.4 이하에서는 패키지 내부에서 import string을 실행하면, Python이 먼저 패키지 디렉토리 내부에서 상대 import를 시도한다.
# 디렉토리 구조
pkg/
  __init__.py
  main.py
  string.py      # 사용자가 만든 모듈
# pkg/main.py (Python 2.4 이하)
import string  # 어떤 string을 import할까?

print(string)
  • Python 2.4 이하에서는 표준 라이브러리의 string 모듈 대신 같은 패키지 내의 pkg/string.py 를 import한다. 이것이 암묵적 상대 import(implicit relative import) 문제 다. 그래서 최상단에 from __future__ import absolute_import 를 사용해 이를 해결했다.
# pkg/main.py (Python 2.5+)
from __future__ import absolute_import

import string  # 이제 표준 라이브러리의 string을 import!

# 같은 패키지의 string.py를 import하려면 명시적으로:
from . import string as pkg_string  # 명시적 상대 import
# 또는
from pkg import string as pkg_string  # 절대 import

만약 여러분들이 매우 과거 python 과 씨름중이라면,, 레거시와 싸우고 있다면,, 이를 꼭 알아야 한다..!

5) 모듈화 원칙과 import/패키지 전략 팁!

아래 내용은 디자인패턴을 차치하고 관점을 SWE의 "모듈화" 에 맞춘 최소한의 가이드에 가깝다.

  1. 단일 책임 원칙(Single Responsibility Principle): 모듈이나 패키지는 하나의 역할에 집중하도록 설계한다. 예를 들어 api/, core/, utils/ 처럼 기능별로 디렉터리를 나누고, 각 디렉터리에는 그 책임에 맞는 모듈만 포함시킨다.

  2. 관심사 분리(Separation of Concerns): 계층 구조를 명확히 분리하여, 예컨대 api 패키지는 입출력(플라스크 엔드포인트 등)만, core 는 비즈니스 로직만, adapters 는 DB나 외부 API 연동만 담당하도록 한다. 이렇게 나누면 import 방향도 한쪽으로 흐르게 되어(상위 계층 -> 하위 계층) 순환 의존을 피할 수 있다.

  3. 안정된 API 노출: 내부 구현 세부 사항은 숨기고, 각 계층의 공개 API만 init.py 등을 통해 노출한다. 예를 들어 core/__init__.py 에서 핵심 서비스 함수만 import하여 외부에 공개하면, 바깥에서는 core.run() 처럼 사용하고 내부 구조가 바뀌어도 인터페이스는 유지할 수 있다. (다만 public, common API는 변경 시 호환성에 주의해야 하므로, 팀 합의하에 버전 관리나 deprecation 절차를 둔다.)

  4. 배포 관점: 실제 패키징(배포)할 때는 폴더 구성과 pyproject.toml/setup.cfg 메타데이터 등이 일관되어야 한다. 이는 PyPA(Python Packaging Authority) 에서 제공하는 가이드에 따라 설계한다. 프로젝트 루트에 pyproject.toml 설정을 두고, 패키지 폴더들은 src/ 디렉터리를 활용하는 등의 표준 패키징 레이아웃을 따르면 배포 시 문제가 적다. 또한 namespace 패키지를 쓸 때는 관련된 모든 배포 패키지에서 __init__.py 를 빼야 한다는 점도 유의한다. (한 군데라도 넣으면 네임스페이스 패키지가 제대로 동작하지 않는다.) PS. 참고로 PyPA는 "파이썬 패키징 관리 그룹" 이다.

my_project/
    api/          # 외부 요청/응답 처리 (입출력 계층)
        __init__.py   (공개 API 재노출)
        ...  
    core/         # 핵심 도메인 로직 (엔진/서비스 계층)
        __init__.py   (공개 API 재노출)
        ...
    adapters/     # DB, 외부 API 등 인프라 연동
        __init__.py   (필요한 경우 설정)
        ...
    utils/        # 여러 곳에서 쓰이는 공용 유틸리티
        __init__.py   (특별한 초기화는 없음)
        ...
  • 각 패키지의 __init__.py 에는 해당 계층에서 외부에 공개할 필요한 심볼들만 임포트하여 노출한다. 예를 들어 core/__init__.py 에는 from .service import run 정도만 넣고 나머지 내부 구현은 숨긴다.

  • 이렇게 하면 상위 레벨에서 from my_project.core import run 처럼 명확하게 쓸 수 있고, 내부 구조 변경이 있어도 run의 인터페이스만 유지하면 된다. 반면 utils 처럼 단순 헬퍼 모음이라면 굳이 __init__ 에 재노출을 하지 않고 각 모듈을 필요한 곳에서 직접 import 해 써도 무방하다.


출처

profile
🔥 도메인 중심의 개발, 깊이의 가치를 이해하고 “문제 해결의 본질” 에 몰두하는 software/product 개발자, 정현우 입니다.

0개의 댓글