Python 백엔드 폴더 철학
Python 백엔드 폴더 철학
Python 웹 백엔드의 폴더 구조는 프레임워크가 강제하지 않는 만큼 팀의 합의에 맡겨집니다. 잘 정렬된 구조는 신규 인력의 온보딩 시간을 줄이고, 그렇지 않으면 같은 패턴이 여러 곳에 흩어집니다.
1. 4 계층 출발점
널리 쓰이는 한 가지 출발점은 도메인 주도 설계의 어휘를 빌린 4 계층입니다.
| 레이어 | 책임 |
|---|---|
routers/ (또는 api/) |
HTTP 진입점. 요청·응답 변환, 인증·검증. |
services/ |
비즈니스 규칙. 트랜잭션 경계. |
repositories/ (또는 dao/) |
DB 접근. SQL · ORM 호출. |
schemas/ |
Pydantic 모델. 입출력 직렬화. |
여기에 운영 성격에 따라 utils/ · crawlers/ · schedulers/ · clients/ · workers/ 같은 폴더가 더해집니다.
2. 도메인 분리
라우터를 도메인별 패키지로 나누면 한 도메인의 변경이 다른 영역에 침범하지 않습니다.
src/
├── routers/
│ ├── users/
│ │ ├── __init__.py # router 인스턴스
│ │ ├── handlers.py
│ │ └── deps.py # 도메인 전용 의존
│ └── orders/
│ └── ...
├── services/
│ ├── users.py
│ └── orders.py
├── repositories/
│ ├── users.py
│ └── orders.py
├── schemas/
│ ├── users.py
│ └── orders.py
├── utils/
│ ├── http.py # 중앙화된 HTTP 유틸
│ ├── time.py
│ └── logger.py
├── crawlers/
│ └── public_data/...
├── schedulers/
│ └── jobs.py
└── main.py
도메인 안에서 다시 레이어를 두기보다, 레이어 폴더 안에 도메인 파일을 두는 형태가 임포트 경로를 짧게 만듭니다. 절대 정답은 없고, 도메인 수가 폭발하면 도메인 우선 폴더로 뒤집는 것이 자연스럽습니다.
3. 중앙화된 HTTP 유틸
여러 곳에서 외부 API 를 직접 requests.get() 으로 호출하면 다음이 흩어집니다.
- User-Agent 정책
- 타임아웃 기본값
- 재시도/백오프
- 에러 로깅·메트릭
- 프록시·인증 헤더
이를 utils/http.py 같은 단일 진입점으로 묶으면 정책 변경이 한 곳에서 끝납니다.
# utils/http.py
import httpx
from tenacity import retry, wait_exponential_jitter, stop_after_attempt
DEFAULT_TIMEOUT = httpx.Timeout(10.0, connect=5.0)
USER_AGENTS = [...]
@retry(wait=wait_exponential_jitter(initial=1, max=10), stop=stop_after_attempt(3))
async def fetch(url: str, **kw) -> httpx.Response:
async with httpx.AsyncClient(timeout=DEFAULT_TIMEOUT, headers={'User-Agent': pick_ua()}) as c:
r = await c.get(url, **kw)
r.raise_for_status()
return r
크롤러·외부 통합은 가능한 한 이 유틸을 통해 호출합니다. 직접 httpx/requests 를 쓰는 자리가 늘어나면 정책 일관성이 깨집니다.
4. 직접 의존 vs 추상
services/ 가 repositories/ 의 함수를 직접 import 해도 무방합니다. 인터페이스 (Protocol) 추상은 테스트 더블이 자주 필요한 자리에서만 도입하면 충분합니다. 모든 자리에 추상을 끼우면 코드량만 늘어납니다.
5. HTTP 클라이언트 후보
| 라이브러리 | 첫 릴리스 | 성격 |
|---|---|---|
requests |
2011, Kenneth Reitz | 동기. 가장 보편적. async 미지원. |
aiohttp |
2014 | async 클라이언트·서버. 오래된 만큼 안정. |
httpx |
2019, encode | 동기·async 양쪽. requests 와 유사한 API. HTTP/2 지원. |
urllib3 |
2008 | requests 의 내부에서도 쓰입니다. 저수준. |
aiohttp-retry · tenacity |
— | 재시도 정책 보조. |
httpx 는 동기 코드와 async 코드가 섞이는 자리에서 일관된 API 를 제공한다는 점이 자주 강조됩니다. requests 는 단순 스크립트·동기 백엔드에서 여전히 직관적입니다.
6. init.py · 설정 모듈 · DI
각 도메인 폴더의 __init__.py 는 외부에 노출할 심볼만 모읍니다. 내부 모듈 구조가 바뀌어도 외부 임포트가 깨지지 않습니다.
core/config.py 또는 settings.py 에 Pydantic Settings (pydantic-settings) 로 환경변수를 한 곳에서 읽습니다. 모듈 곳곳에서 os.getenv 를 흩어 쓰지 않습니다.
FastAPI 의 Depends 함수는 deps.py 같은 파일에 모읍니다. 라우터 파일은 핸들러 본체에 집중합니다.
7. 자주 걸리는 자리
services 가 사실상 라우터 — 비즈니스 로직 없이 리포지토리 호출만 위임하면 계층이 의미를 잃습니다. 단순 CRUD 라면 라우터에서 리포지토리를 직접 호출하고 규칙이 자라면 그때 services 를 도입해도 늦지 않습니다.
순환 임포트 — services/users.py ↔ services/orders.py. 도메인 간 호출이 필요하면 한쪽이 인터페이스만 임포트하거나, 통합 서비스를 별도 모듈로 둡니다.
utils/ 의 비대화 — 분류가 안 되는 코드의 무덤이 되기 쉽습니다. 임계점을 넘으면 도메인이나 어댑터로 승격해 분리합니다.
HTTP 정책의 우회 — 한 곳에서 빠르게 작성하느라 직접 requests.get 을 쓴 자리가 늘면 중앙 유틸의 가치가 사라집니다. 코드 리뷰·린트 룰로 보완합니다.
하고픈 말
폴더 구조는 한 번에 정답이 나오기 어렵습니다. 한 도메인이 자라면 자연스럽게 분리할 자리가 보이고, 여러 도메인이 섞이면 다시 합쳐야 할 자리가 보입니다. 4 계층 + 도메인 폴더 + 중앙 utils 가 작은 팀의 가장 단순한 출발점입니다.
Next
- sql-as-ssot
- api-handler-pattern
httpx 공식 · requests 공식 · aiohttp 공식 · tenacity · Pydantic Settings · Architecture Patterns with Python 을 참고합니다.