파이썬 프로젝트가 커지면 여러 파일을 체계적으로 관리하는 것이 중요합니다. 이 챕터에서는 관련 있는 모듈들을 묶어 패키지로 만드는 방법과 그 핵심 요소인 __init__.py 파일의 역할에 대해 자세히 알아보겠습니다. 코드를 깔끔하게 정리하여 재사용성을 높이는 기초를 다지는 시간입니다.
핵심 요약
- 패키지: 파이썬 모듈들을 논리적으로 묶어주는 디렉토리입니다.
- __init__.py: 패키지임을 알리고, 패키지 초기화 코드를 담는 특별한 파일입니다.
- 구조화: 코드를 더 효율적으로 관리하고 재사용성을 높이는 핵심 전략입니다.
- import 문: 모듈뿐 아니라 패키지 전체 또는 특정 부분도 가져올 수 있습니다.
패키지란 무엇이며 왜 필요한가요?
패키지는 관련 모듈들을 하나의 큰 묶음으로 정리하여 코드를 체계적으로 관리하는 파이썬의 핵심 기능입니다.
모듈의 개념에서 모듈이 개별 파이썬 파일을 의미한다고 배웠습니다. 패키지는 이러한 모듈들을 담는 “디렉토리”라고 생각하시면 됩니다. 제가 처음 복잡한 데이터 분석 프로젝트를 진행했을 때, 수십 개의 `.py` 파일이 뒤죽박죽 섞여 있어 특정 기능을 찾기 어려웠던 경험이 있습니다. 그때 패키지를 활용하기 시작했고, 코드의 가독성과 유지보수성이 비약적으로 향상되는 것을 직접 경험했습니다.
이는 마치 책의 챕터를 폴더로, 각 챕터 안의 페이지를 모듈 파일로 생각하는 것과 같습니다. 서로 관련된 챕터(패키지)들이 모여 한 권의 책(프로젝트)을 이루는 것이지요.
왜 패키지가 필요할까요?
프로젝트 규모가 커질수록 기능별로 코드를 분리하는 것은 필수입니다. 예를 들어, 데이터 처리, 시각화, 보고서 생성 기능을 각각 다른 모듈로 만들 수 있습니다. 이러한 모듈들을 다시 상위 개념으로 묶어주는 것이 바로 패키지입니다. 이렇게 하면 다음과 같은 장점을 얻을 수 있습니다:
- 코드 구성의 체계화: 기능별로 깔끔하게 정리되어 어떤 코드가 어디에 있는지 쉽게 파악할 수 있습니다.
- 이름 충돌 방지: 같은 이름의 함수나 변수가 다른 패키지에 있어도 서로 간섭하지 않습니다.
- 재사용성 증대: 잘 만들어진 패키지는 다른 프로젝트에서도 쉽게 가져다 쓸 수 있어 개발 시간을 단축합니다.
패키지의 기본 디렉토리 구조 만들기
패키지는 단순한 디렉토리이며, 이 안에 최소한 하나의 __init__.py 파일이 존재해야 파이썬이 이를 패키지로 인식합니다.
패키지를 만드는 방법은 생각보다 간단합니다. 일반적인 디렉토리를 만들고 그 안에 `__init__.py`라는 이름의 파일을 추가하면 됩니다. (2026년 기준, 파이썬 3.3 버전부터는 `__init__.py` 파일이 없어도 패키지로 인식되기도 하지만, 여전히 많은 도구와 관례상 이 파일을 포함하는 것이 좋습니다. 특히 초기화 로직이 필요할 때는 필수입니다.)
다음은 간단한 `my_package`라는 패키지를 만드는 예시 구조입니다.
my_project/├── main.py└── my_package/ ├── __init__.py ├── module_a.py └── sub_package/ ├── __init__.py └── module_b.py
위 구조에서 `my_package`와 `sub_package`는 모두 패키지입니다. `module_a.py`와 `module_b.py`는 각각의 패키지 안에 있는 모듈 파일입니다.
__init__.py 파일의 역할 이해하기
__init__.py 파일은 파이썬에게 해당 디렉토리가 패키지임을 알려주고, 패키지가 임포트될 때 초기화 작업을 수행하는 역할을 합니다.
이 파일은 패키지가 임포트될 때 자동으로 실행되는 코드들을 담을 수 있습니다. 예를 들어, 패키지 내의 특정 함수나 클래스를 외부로 쉽게 노출시키거나, 패키지 전역에서 사용할 상수를 정의하는 데 활용됩니다.
__init__.py 파일이 비어 있어도 패키지로서의 기능은 수행하지만, 실제 프로젝트에서는 초기화 로직을 담는 경우가 많습니다.
패키지 초기화 및 외부 노출
우선 `my_package/module_a.py`와 `my_package/sub_package/module_b.py` 파일을 만들어 보겠습니다.
# my_package/module_a.pydef greet(name): return f"안녕하세요, {name}님! module_a에서 인사드립니다."my_variable = "모듈 A의 중요한 데이터"
이 코드가 하는 일: `greet` 함수와 `my_variable`이라는 변수를 정의합니다.
# my_package/sub_package/module_b.pydef calculate_sum(a, b): return a + bclass Calculator: def __init__(self, value): self.value = value def add(self, num): return self.value + num
이 코드가 하는 일: `calculate_sum` 함수와 `Calculator` 클래스를 정의합니다.
이제 `my_package/__init__.py` 파일에 코드를 추가하여 외부에서 패키지를 임포트할 때 어떤 일들이 일어나는지 살펴보겠습니다.
# my_package/__init__.pyprint("my_package를 초기화합니다.")# module_a의 greet 함수를 my_package 이름으로 바로 접근 가능하게 만듭니다.from .module_a import greetfrom .sub_package.module_b import calculate_sum, Calculator__all__ = ["greet", "calculate_sum", "Calculator"] # 'from my_package import *' 시 노출될 항목 지정
이 코드가 하는 일: `my_package`가 임포트될 때 “my_package를 초기화합니다.”라는 메시지를 출력하고, `greet` 함수와 `calculate_sum` 함수, `Calculator` 클래스를 패키지 최상위에서 바로 접근할 수 있도록 노출시킵니다. `__all__` 변수는 모듈의 개념에서 다뤘던 `from … import *` 구문으로 패키지를 임포트할 때 어떤 이름들을 가져올지 명시적으로 제어할 수 있게 합니다. 저는 주로 명확한 접근을 위해 특정 함수나 클래스를 명시적으로 임포트하는 방식을 선호합니다.
패키지 임포트와 사용 방법
패키지를 사용하려면 import 문을 통해 모듈을 가져오듯이 패키지 전체나 그 안의 특정 요소를 지정하여 가져올 수 있습니다.
이제 `my_project` 디렉토리의 루트에 있는 `main.py` 파일에서 우리가 만든 패키지를 어떻게 사용하는지 살펴보겠습니다.
# main.py# 1. 패키지 전체를 임포트import my_packageprint(my_package.greet("바이브 코더")) # my_package.__init__.py에서 노출시킨 greet 함수 사용print(my_package.my_variable) # module_a의 변수는 my_package를 통해 직접 접근 불가능 (my_package.__init__.py에 정의된 바 없음)# 2. 패키지 내 특정 모듈 임포트from my_package import module_aprint(module_a.greet("독자 여러분"))print(module_a.my_variable)# 3. 서브 패키지 내 특정 모듈 임포트from my_package.sub_package import module_bprint(module_b.calculate_sum(10, 20))calc = module_b.Calculator(50)print(calc.add(20))# 4. __init__.py에서 직접 노출시킨 요소 임포트 (더 간결한 접근)from my_package import greet, calculate_sum, Calculatorprint(greet("초보 개발자"))print(calculate_sum(5, 7))calc2 = Calculator(100)print(calc2.add(25))
이 코드가 하는 일: `my_package`와 그 안의 모듈, 그리고 `__init__.py`를 통해 노출된 함수와 클래스를 다양한 방식으로 임포트하고 사용합니다. 제가 처음 패키지를 배울 때 가장 혼란스러웠던 점은 `import my_package`만 했을 때 `module_a`나 `module_b`의 모든 내용이 자동으로 `my_package` 아래로 들어오는 것이 아니라는 사실이었습니다. 모듈 임포트와 마찬가지로, `__init__.py`에서 명시적으로 `from .module_a import some_function`과 같이 정의해주지 않으면, `my_package.some_function`으로는 접근할 수 없습니다. 이 점을 이해하는 데 시간이 꽤 걸렸습니다.
상대 경로 임포트 이해하기
상대 경로 임포트는 패키지 내부에서 다른 모듈이나 서브 패키지를 참조할 때 사용하는 방식으로, 패키지의 재사용성을 높여줍니다.
위 예시에서 `my_package/__init__.py` 파일을 다시 보시면 `from .module_a import greet`와 같은 구문을 사용했습니다. 여기서 `.module_a`는 현재 패키지(`.`) 내의 `module_a`를 의미합니다. 마찬가지로 `..`는 상위 패키지를 의미합니다.
예를 들어, `my_package/sub_package/module_b.py`에서 `module_a`의 `greet` 함수를 사용하고 싶다면 이렇게 작성할 수 있습니다.
# my_package/sub_package/module_b.py# from .module_a import greet (잘못된 예시: 같은 서브패키지 내에 module_a가 없음)from ..module_a import greet # 상위 패키지(my_package)의 module_a 임포트def calculate_sum(a, b): return a + bclass Calculator: def __init__(self, value): self.value = value def add(self, num): print(greet(f"덧셈 계산 중인 {num}")) # module_a의 greet 함수 사용 return self.value + num
이 코드가 하는 일: `module_b`에서 상위 패키지에 있는 `module_a`의 `greet` 함수를 상대 경로로 임포트하여 사용합니다. 상대 경로 임포트는 패키지를 나중에 다른 위치로 옮기거나 이름을 변경할 때도 코드 수정 없이 작동한다는 장점이 있습니다. 다만, 상대 경로 임포트는 패키지 내부에서만 작동하며, 메인 스크립트(`main.py`)와 같이 패키지 외부에 있는 파일에서는 사용할 수 없습니다. 이 점을 유의해야 합니다.
흔한 실수와 해결법
패키지 구조를 잘못 설정하거나 임포트 경로를 틀리는 것이 가장 흔한 실수이며, 파이썬 인터프리터가 알려주는 에러 메시지를 잘 확인해야 합니다.
- ModuleNotFoundError 또는 ImportError: 가장 흔하게 마주치는 에러입니다.
- 원인: `__init__.py` 파일이 없거나, 임포트 경로가 잘못되었을 때 발생합니다.
- 해결:
- 해당 디렉토리에 `__init__.py` 파일이 존재하는지 확인하십시오.
- 임포트하려는 모듈이나 패키지의 이름과 실제 파일/디렉토리 이름이 일치하는지 확인하십시오. 파이썬은 대소문자를 구분합니다.
- 상대 경로 임포트(`.`, `..`)는 패키지 내부 모듈에서만 사용해야 하며, 최상위 스크립트에서는 절대 경로 임포트를 사용해야 합니다.
- 프로젝트의 최상위 디렉토리가 파이썬 경로에 포함되어 있는지 확인해야 할 때도 있습니다. 보통 `python main.py`와 같이 실행하면 현재 디렉토리가 자동으로 포함됩니다.
제가 겪었던 한 가지 예시는, 패키지 안의 모듈을 임포트하면서 `from my_package.module_a import greet` 대신 `import module_a`라고만 쓰는 경우였습니다. 이러면 파이썬은 현재 디렉토리에서 `module_a`를 찾으려 하기 때문에 `ModuleNotFoundError`가 발생합니다. 항상 패키지 이름부터 명확히 지정하여 임포트하는 습관을 들이는 것이 중요합니다.
결론: 잘 정돈된 코드로 더 큰 프로젝트를
패키지는 파이썬 프로젝트를 체계적으로 조직하고 관리하는 데 필수적인 개념이며, 이를 통해 더 크고 복잡한 애플리케이션 개발의 기반을 마련할 수 있습니다.
지금까지 모듈의 개념을 넘어 여러 모듈을 효과적으로 묶는 패키지 생성과 활용법에 대해 알아보았습니다. __init__.py 파일의 중요성과 패키지 디렉토리 구조의 이해는 여러분의 코드를 더욱 견고하고 유지보수하기 쉽게 만들어 줄 것입니다.
제가 처음 자동화 툴을 만들 때, 모든 기능을 하나의 파일에 넣었다가 나중에 기능을 추가하거나 수정할 때마다 코드를 뒤지는 데 시간을 엄청나게 낭비했습니다. 패키지 개념을 적용한 후에는 특정 기능 수정이 필요한 경우 해당 패키지와 모듈만 확인하면 되니 개발 효율이 30% 이상 증가했다고 체감합니다. 여러분도 이 개념을 잘 활용하여 효율적인 파이썬 개발자로 성장하시기를 바랍니다. 이제는 pip를 이용한 외부 패키지 설치 방법을 알아보며, 이미 잘 만들어진 패키지를 활용하는 법을 익혀보겠습니다.
FAQ
- Q: __init__.py 파일이 꼭 필요한가요?
A: 파이썬 3.3 버전부터는 __init__.py 파일이 없어도 디렉토리를 패키지로 인식합니다. 하지만 관례상, 그리고 패키지 초기화 로직이나 __all__ 같은 메타데이터를 정의해야 할 때 여전히 필요한 파일입니다. 안정적인 패키지 구성을 위해 포함하는 것을 권장합니다. - Q: 서브 패키지도 __init__.py 파일이 필요한가요?
A: 네, 그렇습니다. 서브 패키지도 독립적인 패키지이므로, 해당 디렉토리가 패키지임을 파이썬 인터프리터에게 알리기 위해 반드시 __init__.py 파일을 포함해야 합니다. - Q: __all__ 변수는 언제 사용하나요?
A: __all__ 변수는 `from 패키지_이름 import *` 구문을 사용할 때 임포트될 모듈, 함수, 클래스 등을 명시적으로 정의하는 데 사용됩니다. 이 변수를 설정하지 않으면 `import *` 시 예상치 못한 동작이 발생할 수 있습니다. 저는 `import *` 사용을 지양하지만, 라이브러리 개발 시에는 유용하게 쓰입니다.
참고 자료
- [1] Python 공식 튜토리얼 — 모듈 — https://docs.python.org/ko/3/tutorial/modules.html
- [2] Python 패키징 안내 — 패키지 설치하기 — https://packaging.python.org/en/latest/tutorials/installing-packages/
