- 0. 서론: 제발 sys.path.append 좀 그만 써라
- 1. Src Layout: 왜 굳이 src 폴더를 파는가?
- 2.
pyproject.toml: 패키지의 신분증 - 3. Editable Install: 수정할 때마다 재설치? No.
- 4.
__init__.py: 패키지의 얼굴마담 (Facade Pattern) - 5. 앞으로 당장 해야 할 일
- 6. 실제 개발 과정
0. 서론: 제발 sys.path.append 좀 그만 써라
import sys
sys.path.append("../src") # 제발 나가 뒤져라
from my_model import UNet
이건 “내 컴퓨터, 내 폴더 구조에서만 돌아가는 쓰레기 코드” 임. 남의 컴퓨터 가면 경로 달라서 바로 터져버린다.
내 목표는 만든 코드를 팀원이 어느 폴더에 있든 상관없이 이렇게 쓰게 만드는 거임.
# 깔끔 그 자체
from captain_diffusion import UNet
더 나아가서 내가 개발 뚝딱 해놓으면 팀원이 내 repo를 로컬에 안받고, 내 repo 통째로 pip install git+~~ 설치해서 재현성 확보하며 협업을 할 수 있게됨.
이렇게 하려면 폴더 구조부터 갈아엎어야 함.
1. Src Layout: 왜 굳이 src 폴더를 파는가?
보통 나는 프로젝트 루트에 소스 코드(model.py, train.py)를 다 때려 박는 Flat Layout을 쓰고있는데 이게 왜 망하는 지름길이었을까.
구조 비교
❌ Flat Layout (아마추어 방식)
my_project/
├── my_package/ # 패키지 폴더
│ ├── __init__.py
│ └── model.py
├── tests/ # 테스트 폴더
│ └── test_model.py
├── pyproject.toml
└── train.py # 실행 스크립트가 뒹굴러 다님
✅ Src Layout (프로 방식)
my_project/
├── src/ # 소스 코드를 격리함
│ └── my_package/ # 진짜 패키지는 여기 숨어있음
│ ├── __init__.py
│ └── model.py
├── tests/
│ └── test_model.py
├── pyproject.toml
└── train.py
왜 src를 써야 하는가? (Import Hell 방지)
파이썬은 import를 할 때 “현재 작업 디렉토리(Current Working Directory)” 를 sys.path의 맨 앞에 추가하는데
상황: Flat Layout에서 테스트를 돌릴 때
- 프로젝트 루트에서 pytest를 실행.
- sys.path 맨 앞에
my_project/(루트)가 추가됨. import my_package를 하면, 파이썬은 설치된 라이브러리를 찾는 게 아니라, 그냥 현재 폴더에 있는my_package폴더를 바로 가져옴.
문제점
- “설치가 개판으로 되어도 테스트가 통과함”: 패키징 설정(
pyproject.toml)을 잘못해서 실제로는 설치가 안 되는 상태여도, 로컬 폴더에 파일이 있으니까 테스트가 통과. - 배포 후 폭망: CI/CD 서버나 팀원 컴퓨터에서는 설치가 제대로 안 돼서 에러가 터짐.
해결책: Src Layout
- my_package가
src밑에 숨어있음. - 프로젝트 루트에서
import my_package를 하면 못 찾음. (현재 폴더에 없으니까) - 강제로 설치(
pip install -e .)를 해야만 import가 가능. - 즉, “개발 환경에서도 실제 설치된 상태와 똑같이 테스트” 가 가능함.
- 실제 설치된 상태란? 팀원이 내꺼 repo를
git clone말고pip install로 설치했을 때를 말함
- 실제 설치된 상태란? 팀원이 내꺼 repo를
2. pyproject.toml: 패키지의 신분증
이제 GAN시절 쓰던 setup.py는 뒤졌음. pyproject.toml이 표준임. uv init하면 생기는데, 이걸 어떻게 설정하느냐가 매우 중요함.
Build System (누가 포장을 할 것인가?)
# pyproject.toml
# 패키지를 빌드(포장)해줄 도구
# 요즘은 hatchling을 많이 씀. (setuptools는 너무 낡았음)
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "captain-diffusion"
version = "0.1.0"
description = "내가 만든 개쩌는 디퓨전 모델"
requires-python = ">=3.11"
dependencies = [
"torch>=2.1.0",
"numpy",
]
# [중요] 패키지 위치 지정
# src 레이아웃을 쓰면 도구가 못 찾을 수 있어서 명시해줌
[tool.hatch.build.targets.wheel]
packages = ["src/captain_diffusion"]
이 파일이 있어야 pip install . 같은 명령어가 먹힘
잠깐! ‘빌드’가 뭐임?
시프 수업 때 먼지도 모르고 make 어쩌고 했던걸 빌드라 했던거같은데 C언어말고 파이썬도 빌드를 해야함?
- 소스 코드(Raw Material): 내가 짠 py 파일들. 그냥 텍스트 쪼가리임.
- 빌드(Build): 이 소스 코드를 남들이 설치하기 좋은 형태(Wheel 파일,
.whl)로 포장하는 과정. 마트에서 파는 밀키트처럼 만들어줌.- 빌드없이 공유하면 그냥 냄비 통째로 팀원에게 들고가라 하는거랑같다. 나는 일단햇음 ㅅㄱ 느낌. (팀원이 가다가 다 흘리고, 새똥맞고 개판됨)
pyproject.toml의 역할 (PEP 517 표준)
- 과거엔
setup.py(파이썬 스크립트)를 실행해서 빌드했음. - 보안 문제도 있고, 아무튼 별로라고함.
- 이제는
pyproject.toml이라는 명세서를 쓴다.
빌드 과정
- Frontend (
pip): “야, 이 폴더 빌드해서 설치하라.” pyproject.toml확인: “어?build-backend = hatchling이라고 써있네? 해치링 가온나.”- Backend (
hatchling): “예 주인님, 명세서대로src폴더 묶고 의존성 적어서.whl파일 뱉겠습니다.” - 설치: 만들어진
.whl을site-packages에 복사.
즉, 이 파일은 “누가 요리사(Backend)이고 레시피(Metadata)는 무엇인지” 적혀있는 주문서임.
이거 없으면 pip는 뭘 해야 할지 몰라서 에러 뱉고 눕는다.
3. Editable Install: 수정할 때마다 재설치? No.
여기서 나올 수 있는 의문
Q. 아니 src에 넣으면 import 안 되자늠; 코드 한 줄 고칠 때마다 pip install 다시 해야 됨? 개귀찮노ㅡㅡ
A. pip install . -> uv pip install -e .
이거 이해 못 하면 하루에 pip install만 500번 하다가 퇴근함
명령어
# 개발 모드로 설치 (뒤에 점 . 찍는 거 필수)
uv pip install -e .
# 또는
pip install -e .
-e(Editable): “편집 가능한 상태로 설치해라.” 즉, 파일을 복사하지 말고 원본 위치를 참조하게 해라..(Dot): “현재 디렉토리(Current Directory)를 설치해라.” 정확히는pyproject.toml이 있는 이 폴더를 패키지로 인식해라.
일반 설치 (pip install .) vs Editable 설치 (pip install -e .)
| 비교 항목 | 일반 설치 (pip install .) |
Editable 설치 (pip install -e .) |
|---|---|---|
| 동작 원리 | 소스 코드를 복사(Copy)해서 site-packages에 넣음. | 소스 코드의 위치(Path)만 site-packages에 적어둠. |
| 코드 수정 시 | 설치된 파일은 옛날 거임. 수정이 반영 안 됨. | 원본을 바로 읽어오므로 수정이 즉시 반영됨. |
| 개발 편의성 | 최악 (고칠 때마다 재설치해야 함) | 최상 (그냥 고치고 실행하면 됨) |
| 언제 쓰나? | 배포할 때 (Docker 이미지 만들 때 등) | 개발할 때 (코드 짜고 있을 때) |
상황 예시 : ./src/models/model.py에서 print("학습 시작")을 print("Start")로 고쳤음.
- 일반 설치: 실행하면 여전히 “학습 시작”이라고 뜸. 열받아서 모니터 뿌셔짐. ->
pip install .다시 해야 바뀜. - Editable 설치: 실행하면 바로 “Start”라고 뜸고 바로 퇴근함.
4. __init__.py: 패키지의 얼굴마담 (Facade Pattern)
작업 하다 보면 모델 파일안에 여러가지 함수, 클래스가 들어가있을 수 있음
- ex)
diffusion_model,scheduler,diffusion_model_2,diffusion_model_3,get_timestep
뭐하나 추가될 때마다 from models.diffusion_model, ... 해서 존나 길어지거나 import 문으로 20줄까지 늘어날 수 잇음.
__init__.py를 잘 써 깔끔하게 포장(Facade) 하서.
폴더 구조 예시
src/captain_diffusion/
├── __init__.py <-- 여기가 핵심
└── models/
├── __init__.py
└── unet.py <-- 여기에 class UNet 정의됨
└── scheduler.py <-- 여기에 학습 관련 함수 정의됨
__init__.py 작성법
src/captain_diffusion/models/__init__.py 안에 자주쓰일꺼같은 놈들 미리 박아둠
# 하위 모듈에 있는 UNet을 이 파일 레벨로 끌어올림
from .unet import UNet
from .scheduler import get_flow_discrete_schedule, get_timestep
src/captain_diffusion/__init__.py (최상위)
# models 패키지에 있는 UNet을 최상위로 끌어올림
from .models import UNet
from .scheduler import get_flow_discrete_schedule, get_timestep
# 버전 정보 노출
__version__ = "0.1.0"
결과: 사용자 경험(UX)의 차이
- Before (좆같음)
from captain_diffusion.models.unet import UNet
from captain_diffusion.scheduler import get_flow_discrete_schedule, get_timestep
from captain_diffusion.optimizer import sex
...
# 대략 500줄
- After (편안~)
import captain_diffusion as cd
model = cd.UNet() # 와! 사용하기 너무 편하다!
print(cd.__version__)
timestep = cd.get_timestep(a, b, c, d)
5. 앞으로 당장 해야 할 일
- 폴더 이동: 지금 루트에 널브러진
model.py,utils.py싹 다 잡아서src/SOME_THING/폴더 만들고 그 안으로 유배 보내기. - Config 작성:
pyproject.toml열어서 [build-system]이랑 [project] 섹션 채우기. (복붙 하지 말고 프로젝트 이름 맞게 수정) - 설치: 터미널 열고
uv pip install -e .실행. - Import 정리: 각 폴더마다
__init__.py만들어서 클래스들 예쁘게 노출시키기. - 테스트: 루트에서
python -c "import captain_diffusion; print(captain_diffusion.__file__)"쳤을 때src밑에 있는 경로가 뜨면 성공.
6. 실제 개발 과정
1. 공사 기초 작업 (Scaffolding)
# 프로젝트 생성 및 초기화
mkdir captain-diffusion
cd captain-diffusion
uv init
- 결과:
hello.py,pyproject.toml,.python-version생성됨. - 할 일:
hello.py는 예제 파일이니 바로 삭제.
# src 폴더 내부 직접 깍아야 함.
# __init__.py 빼먹지말고
mkdir -p src/captain_diffusion
touch src/captain_diffusion/__init__.py
2. 명세서 작성 (이거 안 하면 install 실패함)
pyproject.toml 수정. uv init으로 생긴 파일은 껍데기임
[project]
name = "captain-diffusion"
version = "0.1.0"
description = "Project description"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"torch",
"numpy"
]
# --- [이 부분을 직접 추가해야 함] ---
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
# src 레이아웃 쓴다고 명시
[tool.hatch.build.targets.wheel]
packages = ["src/captain_diffusion"]
3. 연결 (The Marriage)
# 가상환경 만들고, 의존성 깔고, 현재 폴더를 편집 모드로 연결
uv pip install -e .
Successfully installed captain-diffusion-0.1.0 뜨면 성공
4. 작업 루프 (TDD 스타일)
- Step A (Test): 프로젝트 루트에
tests/test_model.py생성.
# tests/test_model.py
from captain_diffusion import UNet # 아직 안 만들어서 빨간줄 뜸 (정상)
def test_unet_shape():
model = UNet()
assert model is not None
- Step B (Implement):
src/captain_diffusion/model.py에 가서 코드를 짜준다.
# src/captain_diffusion/model.py
import torch.nn as nn
class UNet(nn.Module):
def __init__(self):
super().__init__()
...
- Step C (Expose):
src/captain_diffusion/__init__.py에 등록. - Step D 루트에서 실행 ㄱㄱ
cd PROJECT_ROOT
uv run pytest
pytest 하면 test/* 내부 파일들 다 실행해 주는데 이건또 규칙이있음. 다음 포스트에.