Haribo ML, AI, MATH, Algorithm

고수들의 repo 안에 src는 뭐하는놈일까

2025-12-06
Haribo

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에서 테스트를 돌릴 때

  1. 프로젝트 루트에서 pytest를 실행.
  2. sys.path 맨 앞에 my_project/ (루트)가 추가됨.
  3. 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로 설치했을 때를 말함

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 파일 뱉겠습니다.”
  • 설치: 만들어진 .whlsite-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/* 내부 파일들 다 실행해 주는데 이건또 규칙이있음. 다음 포스트에.


Similar Posts

이전 포스트 DPO 리뷰

Comments