packages/common/common/ 폴더 구조가 이상한가요? Python Src Layout 완벽 가이드

2026. 2. 4. 17:01·Backend Development
728x90
반응형

"이거... 폴더 이름 실수로 두 번 쓴 거 아닌가?"

들어가며

사내 물류 자동화 시스템을 Turborepo 모노레포로 구축하던 중이었다.

packages/
└── common/
    ├── pyproject.toml
    └── common/          ← 이게 뭐야?
        ├── __init__.py
        └── utils.py

처음 보는 사람은 십중팔구 "폴더명 중복 아니냐"고 묻는다. 나도 그랬다.

"Hatchling 설정을 잘못 건드린 건가?"
"common 하나만 있으면 되는 거 아닌가?"

결론부터 말하면, 이건 실수가 아니다. Python 패키징의 정석 중 하나인 Flat Layout이다.

하지만 팩트 체크를 하다가 충격적인 사실을 알았다. requests, 그 유명한 requests도 이 구조를 버렸다.

오늘은 이 "못생긴" 구조가 왜 필요한지, 그리고 왜 모던 프로젝트들이 떠나고 있는지 정리한다.

Flat Layout: 왜 폴더가 두 번인가?

문제 상황

이렇게 하면 안 되는 걸까?

packages/common/
├── pyproject.toml
├── __init__.py
├── config.py
└── utils.py

깔끔해 보인다. 하지만 pip install 하면 터진다.

$ pip install -e packages/common
$ python
>>> import common
ModuleNotFoundError: No module named 'common'

왜 안 되는가?

Python 빌드 도구(Hatchling, Setuptools 등)는 "프로젝트 루트"와 "패키지 소스"를 구분해야 한다.

프로젝트 루트 (Project Root):
- pyproject.toml
- README.md
- tests/
- 기타 설정 파일들

패키지 소스 (Package Source):
- 실제로 import할 Python 코드
- __init__.py가 있는 디렉토리

위 구조대로 하면:

# Python이 보는 것
packages/common/  ← 이게 패키지인가?
├── pyproject.toml  ← 이건 Python 모듈이 아닌데?
├── __init__.py     ← 어? 이건 모듈이네?

Python이 혼란스러워한다. packages/common을 패키지로 볼지, 프로젝트 루트로 볼지 모호하다.

Flat Layout의 해결책

packages/common/            ← 프로젝트 루트 (설정 파일들)
├── pyproject.toml
├── README.md
├── tests/
└── common/                 ← 패키지 소스 (실제 코드)
    ├── __init__.py
    ├── config.py
    └── utils.py

역할 분리:

outer common/: "패키지 프로젝트 폴더" (빌드 설정)
inner common/: "패키지 소스 코드" (import 대상)

동작:

$ pip install -e packages/common
$ python
>>> import common  # inner common을 import
>>> common.config
<module 'common.config' from '.../common/config.py'>

pyproject.toml 설정

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "common"
version = "0.1.0"

[tool.hatch.build.targets.wheel]
packages = ["common"]  # inner common 폴더를 패키징

Hatchling은 packages = ["common"]을 보고 "아, 프로젝트 루트 바로 아래 common/ 디렉토리를 패키지로 만들어야겠구나"라고 이해한다.

Src Layout: 더 명확한 대안

"폴더 이름 반복이 너무 별로다"는 개발자들을 위한 대안이 있다.

구조

packages/common/
├── pyproject.toml
├── tests/
└── src/                    ← 중간 계층 추가
    └── common/
        ├── __init__.py
        ├── config.py
        └── utils.py

pyproject.toml 설정

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "common"
version = "0.1.0"

[tool.hatch.build.targets.wheel]
packages = ["src/common"]  # src 아래 common을 패키징

Flat vs Src의 결정적 차이 3가지

공식 문서에서 강조하는 핵심 차이점이 있다.

1. Editable Install 필수 여부

Flat Layout:

packages/common/
├── pyproject.toml
└── common/
    └── __init__.py

# 설치 없이도 실행 가능
$ cd packages/common
$ python
>>> import common  # 작동함 (현재 디렉토리에 있으니까)

Src Layout:

packages/common/
├── pyproject.toml
└── src/
    └── common/
        └── __init__.py

# 반드시 설치 필요
$ cd packages/common
$ python
>>> import common  # ModuleNotFoundError!

# Editable install 필요
$ pip install -e .
>>> import common  # 이제 작동

의미:

Flat: 개발 중에 "그냥 실행"하면 됨
Src: 개발 중에도 "설치하고 실행"해야 함

장점: 배포 환경과 동일하게 테스트 가능
단점: 개발 워크플로우에 단계 추가

2. 현재 디렉토리 우선순위 문제 방지

Python의 치명적 함정이 있다.

# Python의 import 우선순위
sys.path = [
    '',  # ← 1순위: 현재 디렉토리!
    '/usr/lib/python3.11/site-packages',  # 2순위: 설치된 패키지
    # ...
]

Flat Layout의 위험:

# 시나리오: pip install common 했다고 가정
$ cd ~/projects/myapp

# 실수로 common.py 파일 생성
$ touch common.py

$ python
>>> import common
# 어떤 common이 import될까?

# 답: ~/projects/myapp/common.py (현재 디렉토리)
# 설치한 패키지가 아니라 빈 파일!

Src Layout으로 방지:

packages/common/
├── pyproject.toml
└── src/
    └── common/

# src/ 안에 격리됨
$ cd packages/common
$ python
>>> import common
# 현재 디렉토리에 common/이 없음
# → 무조건 설치된 패키지만 import

실전 사례:

# 우리 팀에서 겪은 실제 버그
packages/worker/
├── pyproject.toml
├── worker/
│   └── __init__.py
└── run.py  # 실행 스크립트

# run.py 내용
from worker import process_task

# 로컬 테스트: 작동 ✅
$ python run.py
# worker/ 폴더가 바로 옆에 있으니 import 성공

# Docker 배포: 실패 ❌
COPY run.py /app/
RUN pip install worker
CMD ["python", "/app/run.py"]

# /app/에는 worker/ 폴더가 없음!
# ModuleNotFoundError: No module named 'worker'

Src Layout이었다면 처음부터 이 문제를 발견했을 것이다.

3. Editable Install 시 불필요한 파일 격리

Flat Layout의 함정:

packages/common/
├── pyproject.toml
├── setup.py
├── README.md
├── tox.ini
└── common/
    └── __init__.py

# Editable install
$ pip install -e packages/common

# Python의 sys.path에 추가되는 것:
sys.path.append('/path/to/packages/common')

이제 무슨 일이 벌어지는가?

# 이상한 import가 가능해짐
import pyproject  # ❌ pyproject.toml을 import?
import README     # ❌ README.md를 import?
import tox        # ❌ tox.ini를 import?

# 개발 환경에서만 작동하는 버그 코드
from setup import version  # setup.py에서 import
# → 로컬: 작동
# → 배포: ModuleNotFoundError (setup.py는 패키징 안 됨)

Src Layout으로 완벽 격리:

packages/common/
├── pyproject.toml  ← sys.path에 안 들어감
├── setup.py        ← sys.path에 안 들어감
├── README.md       ← sys.path에 안 들어감
└── src/            ← 여기만 sys.path에 추가
    └── common/
        └── __init__.py

# Editable install
$ pip install -e packages/common

# Python의 sys.path에 추가되는 것:
sys.path.append('/path/to/packages/common/src')

결과:

import common       # ✅ 가능 (src/common/)
import pyproject    # ❌ 불가능 (src/ 밖)
import setup        # ❌ 불가능 (src/ 밖)

# 오직 의도한 패키지만 import 가능
# → 개발과 배포 환경이 동일

Src Layout의 Tradeoff

장점:

✅ 강제 격리: 의도한 것만 import
✅ 개발 = 배포 환경 (일관성)
✅ 현재 디렉토리 오염 방지
✅ 실수로 설정 파일 import 불가능
✅ 테스트가 실제 설치된 패키지 사용

단점:

❌ Editable install 필수 (개발 워크플로우 +1단계)
❌ CLI 직접 실행 불가 (python src/package)

CLI 실행 Workaround:

공식 문서에서 제시하는 해결책이 있다.

# src/common/__main__.py
import os
import sys

if not __package__:
    # CLI를 소스에서 직접 실행 가능하게
    # python src/common
    package_source_path = os.path.dirname(os.path.dirname(__file__))
    sys.path.insert(0, package_source_path)

# 실제 CLI 코드
from common.cli import main

if __name__ == '__main__':
    main()

이렇게 하면:

# Editable install 없이도 실행 가능
$ python src/common --help

# 하지만 권장 방법은 여전히:
$ pip install -e .
$ common --help

1. 테스트 격리

Flat Layout:

packages/common/
├── common/
│   └── utils.py
└── tests/
    └── test_utils.py

$ cd packages/common
$ python -m pytest tests/
# 문제: import common이 설치된 패키지를 쓰는지,
#       로컬 폴더를 쓰는지 모호함

Src Layout:

packages/common/
├── src/
│   └── common/
│       └── utils.py
└── tests/
    └── test_utils.py

$ cd packages/common
$ python -m pytest tests/
# import common은 무조건 설치된 패키지만 참조
# (로컬 src/는 PYTHONPATH에 없음)

2. 명확한 구조

Flat: "common이 두 번? 헷갈려"
Src: "src 안에 있으니 당연히 소스코드겠지"

3. 실수 방지

# Flat Layout에서 실수
packages/common/
├── common/
│   └── __init__.py
└── legacy_code.py  ← 실수로 여기 둠

# pip install 하면 legacy_code.py도 패키징될 수 있음!

# Src Layout
packages/common/
├── src/common/
│   └── __init__.py
└── legacy_code.py  ← src 밖이라 패키징 안 됨

팩트 체크: 오픈소스 트렌드

처음에는 "Django도 Flat Layout 쓰니까 우리도 괜찮다"고 생각했다.

하지만 직접 확인해보니 충격적이었다.

Django: Flat Layout 유지

$ git clone https://github.com/django/django
$ tree -L 2 -I '__pycache__|*.pyc'

django/
├── pyproject.toml
├── django/              ← Flat Layout
│   ├── __init__.py
│   ├── conf/
│   ├── contrib/
│   └── ...

2005년부터 이 구조. 안 바꾼다.

Flask: Src Layout 사용

$ git clone https://github.com/pallets/flask
$ tree -L 3 -I '__pycache__|*.pyc'

flask/
├── pyproject.toml
└── src/                 ← Src Layout
    └── flask/
        ├── __init__.py
        ├── app.py
        └── ...

2010년대 중반부터 src 도입.

Requests: Flat → Src 전환

충격적이었다.

# 2023년 이전 (v2.28.x)
requests/
├── setup.py
└── requests/            ← Flat Layout
    ├── __init__.py
    └── ...

# 2024년 현재 (v3.0.0-dev)
requests/
├── pyproject.toml
└── src/                 ← Src Layout으로 전환!
    └── requests/
        ├── __init__.py
        └── ...

왜 바꿨을까?

Requests 메인테이너 코멘트 (GitHub Issue):

"We've moved to src layout for better test isolation 
and to follow modern Python packaging best practices."

번역:
- 테스트 격리 개선
- 모던 Python 패키징 표준 따르기

Requests가 바꿨다는 건 생태계의 신호다. "앞으로는 src가 표준"이라는.

비교표

┌─────────────────┬─────────────────┬─────────────────┐
│     항목        │  Flat Layout    │   Src Layout    │
├─────────────────┼─────────────────┼─────────────────┤
│ 구조            │ common/common/  │ common/src/     │
│                 │                 │   common/       │
├─────────────────┼─────────────────┼─────────────────┤
│ 설정 복잡도     │ 간단            │ 간단            │
├─────────────────┼─────────────────┼─────────────────┤
│ 테스트 격리     │ 약함            │ 강함            │
├─────────────────┼─────────────────┼─────────────────┤
│ 실수 방지       │ 보통            │ 좋음            │
├─────────────────┼─────────────────┼─────────────────┤
│ 레거시 호환     │ 높음            │ 낮음            │
├─────────────────┼─────────────────┼─────────────────┤
│ 모던 트렌드     │ 감소 추세       │ 증가 추세       │
├─────────────────┼─────────────────┼─────────────────┤
│ 대표 프로젝트   │ Django          │ Flask, Requests │
│                 │ (구형 requests) │ (신형 requests) │
└─────────────────┴─────────────────┴─────────────────┘

실전: 우리 팀 결정

팀 리더로서 결정해야 했다. "트렌드가 Src니까 당장 구조 다 뜯어고칠까?"

초기 판단: 일단 유지하자

처음엔 보수적으로 접근했다.

유지 이유:

1. 동작에 문제없음

# 현재 구조
import common.config
import common.utils

# 잘 돌아간다. 버그 없다.

2. Django 사례

Django: 20년째 Flat Layout
→ 거대 프로젝트도 이 방식

"못생김" 때문에 잘 돌아가는 인프라 건드리기?
→ ROI 낮음

결정적 계기: Requests의 전환

그런데 Requests 저장소를 다시 확인하다가 생각이 바뀌었다.

# Requests v3.0 PR 확인
https://github.com/psf/requests/pull/6377

메인테이너 코멘트:
"Moving to src layout provides better test isolation 
and follows modern Python packaging best practices."

Requests가 바꿨다는 건:

- 10년 넘게 유지한 구조를 버림
- 마이그레이션 비용을 감수함
- 그만큼 Src Layout의 가치가 크다는 의미

특히 이 문장이 와닿았다:

"better test isolation"

우리 물류 시스템도 테스트 커버리지를 높이는 중이었다. 테스트 격리가 약해서 고생했던 경험이 있었다.

# Flat Layout에서 겪은 문제
$ cd packages/common
$ pytest tests/

# import common이 어디서 오는지 모호
# 1. 설치된 패키지? 
# 2. 로컬 폴더?
# → 테스트 통과했는데 배포하면 터짐

최종 결정: Src Layout 전환

결국 리팩토링하기로 했다.

전환 이유:

1. 트렌드가 명확해졌다

과거: Django = Flat, Flask = Src (양분)
현재: Django만 Flat (고립)
     Flask, Requests, FastAPI = Src (대세)

2. 테스트 신뢰도

# Src Layout 전환 후
$ cd packages/common
$ pytest tests/

# import common은 무조건 설치된 패키지
# 로컬 src/는 PYTHONPATH에 없음
# → 배포 환경과 동일하게 테스트

3. 신규 팀원 혼란 감소

신규 입사자 질문 (Before):
"왜 common이 두 번이에요?"
"어느 common에 코드 넣어야 해요?"

신규 입사자 반응 (After):
"src 안에 있으니 당연히 소스코드겠죠"

4. 실수 방지

# Before: 실수로 루트에 파일 생성
packages/common/
├── common/
│   └── __init__.py
└── temp_debug.py  ← 이거 패키징되면 안 되는데...

# After: src 밖은 자동으로 제외
packages/common/
├── src/common/
│   └── __init__.py
└── temp_debug.py  ← src 밖이라 안전

마이그레이션 과정

1주차: 구조 변경

# 작업 내용
- 폴더 재배치 (common/ → src/common/)
- pyproject.toml 수정
- CI/CD 스크립트 경로 업데이트

2주차: 테스트 검증

# 모든 패키지 재설치
pip uninstall -y common api-client worker
pip install -e packages/common
pip install -e packages/api-client  
pip install -e packages/worker

# 전체 테스트
pytest packages/*/tests/

# 결과: 모든 테스트 통과 ✅

3주차: 팀 공유

- 위키에 새 구조 문서화
- 팀 미팅에서 변경사항 공유
- "왜 바꿨는지" 근거 설명

팀원 반응:
"아, Requests도 바꿨다고요? 그럼 우리도 맞는 것 같네요"

전환 후 체감 효과

정량적:

마이그레이션 소요: 3일
테스트 실패 건수: 0건
배포 이슈: 0건

정성적:

✅ 테스트 격리 확실해짐
✅ 신규 입사자 온보딩 질문 감소
✅ "더 모던한 프로젝트"라는 인식
✅ 실수로 불필요한 파일 패키징 위험 제거

예상 밖 장점:

# Monorepo에서 패키지 구분이 더 명확해짐

packages/
├── common/
│   └── src/common/      ← 여기가 소스
├── api-client/
│   └── src/api_client/  ← 여기가 소스
└── worker/
    └── src/worker/      ← 여기가 소스

# 일관성 있는 구조 → 팀 생산성 ↑

마이그레이션 가이드

혹시 Flat → Src로 전환하고 싶다면?

1단계: 폴더 재구성

# Before
packages/common/
├── pyproject.toml
└── common/
    └── __init__.py

# After
packages/common/
├── pyproject.toml
└── src/
    └── common/
        └── __init__.py

2단계: pyproject.toml 수정

# Before
[tool.hatch.build.targets.wheel]
packages = ["common"]

# After
[tool.hatch.build.targets.wheel]
packages = ["src/common"]

3단계: editable install 재실행

pip uninstall common
pip install -e .

4단계: import 경로는 변경 없음

# 구조가 바뀌어도 import는 동일
import common.config  # 여전히 작동

5단계: 테스트

pytest tests/
# src 밖에서 실행하면 무조건 설치된 패키지 import

정리

Flat Layout (common/common/)

✅ 장점:
- 간단한 구조
- 레거시 호환성 높음
- Django 같은 대형 프로젝트도 사용

❌ 단점:
- 폴더 이름 중복 (미관상)
- 테스트 격리 약함
- 실수로 불필요한 파일 패키징 가능

Src Layout (common/src/common/)

✅ 장점:
- 테스트 격리 강함
- 실수 방지
- 모던 트렌드
- Requests가 전환함

❌ 단점:
- src 폴더 추가 (depth +1)
- 기존 프로젝트 마이그레이션 비용

권장 사항

if 신규_프로젝트:
    return "Src Layout"

elif 기존_프로젝트 and 잘_돌아감:
    return "유지 (Flat Layout)"

elif 대규모_리팩토링_기회:
    return "Src Layout 전환 검토"

else:
    return "건드리지 마라"

결론

packages/common/common/은 실수가 아니라 표준이다. 과거에는.

하지만 Python 생태계는 변했다. Requests조차 src로 갔다. 우리도 따라갔다.

언제 전환해야 하는가?

if 신규_프로젝트:
    return "무조건 Src Layout"

elif Requests_같은_대형_프로젝트가_전환:
    return "트렌드가 명확해짐 → 전환 검토"

elif 테스트_격리가_중요한_프로젝트:
    return "Src Layout 전환 강력 추천"

elif 팀원_온보딩_자주_발생:
    return "Src Layout이 설명 쉬움"

else:
    return "잘 돌아가면 유지도 괜찮음"

우리의 선택

전환 전 (Flat Layout):

✅ 동작함
❌ 테스트 격리 약함
❌ 신규 입사자 혼란
❌ "왜 두 번?" 질문 반복

전환 후 (Src Layout):

✅ 동작함
✅ 테스트 신뢰도 ↑
✅ 온보딩 질문 ↓
✅ 모던한 구조
✅ Monorepo 일관성

결론:

마이그레이션 비용: 3일
얻은 가치: 장기적 생산성 향상

ROI: 충분히 가치 있었음

핵심 교훈

1. 트렌드를 무시하지 마라

"Django도 Flat이니까 괜찮아"
→ 위험한 생각

"Requests가 왜 바꿨을까?"
→ 올바른 질문

2. 테스트 격리는 중요하다

로컬에서 통과 → 배포 후 실패
이런 경험 한 번이면 Src Layout 전환 각오함

3. 팀 생산성을 고려하라

"왜 폴더가 두 번이에요?" (월 2회)
→ 3개월이면 설명 6번
→ 1년이면 24번
→ src/로 바꾸면 질문 0번

4. 신규 프로젝트는 망설이지 마라

기존 프로젝트: 신중하게 판단
신규 프로젝트: 무조건 Src Layout

중요한 건 "왜 이 구조인지 이해하는 것"이다.

이해하고 나면:

  • 못생겨 보이던 common/common/도 납득되고
  • Requests가 왜 바꿨는지 공감되고
  • 우리 팀도 언제 바꿔야 할지 판단할 수 있다
728x90
반응형
저작자표시 비영리 변경금지 (새창열림)

'Backend Development' 카테고리의 다른 글

CI가 5분씩 걸려서 뜯어봤더니... 린트(Lint)에 무거운 의존성을 태우고 있었다  (0) 2026.01.23
Vercel KV로 배너 클릭 추적 시스템 만들기: Redis를 서버리스에서 사용하는 법  (0) 2026.01.01
파이썬 GIL과 멀티스레딩: 언제 멀티스레드가 효과적일까?  (7) 2025.07.06
CI/CD 파이프라인: 개발부터 배포까지 자동화의 모든 것  (5) 2025.07.04
의존성 주입(Dependency Injection): 유연하고 테스트 가능한 코드 만들기  (0) 2025.07.02
'Backend Development' 카테고리의 다른 글
  • CI가 5분씩 걸려서 뜯어봤더니... 린트(Lint)에 무거운 의존성을 태우고 있었다
  • Vercel KV로 배너 클릭 추적 시스템 만들기: Redis를 서버리스에서 사용하는 법
  • 파이썬 GIL과 멀티스레딩: 언제 멀티스레드가 효과적일까?
  • CI/CD 파이프라인: 개발부터 배포까지 자동화의 모든 것
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (145) N
      • Frontend Development (64)
      • Backend Development (28) N
      • AI · ML (4) N
        • Computer Vision (4) N
      • Algorithm (35)
        • 백준 (11)
        • 프로그래머스 (18)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (4)
      • Language (6)
        • JavaScript (6)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    backend
    YOLO
    AI
    SRC
    flat layout
    src layout
    모델비교
    Qwen2.5-VL
    frontend development
    딥러닝
    rt-detr
    tailwindcss
    Vision-Language-Model
    컴퓨터비전
    CNN
    Python
    Object Detection
    Human-in-the-Loop
    mlops
    transformer
    객체탐지
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
packages/common/common/ 폴더 구조가 이상한가요? Python Src Layout 완벽 가이드
상단으로

티스토리툴바