- 311_FastAPI_구조_원칙.md: 현재 구조가 아닌 지켜야 할 원칙으로 재작성 - 계층 분리, 의존성 방향, DB 접근 규칙 명확화 - 체크리스트 및 예외 상황 추가
177 lines
4.5 KiB
Markdown
177 lines
4.5 KiB
Markdown
# FastAPI 프로젝트 구조 원칙
|
|
|
|
**작성일**: 2025-09-17
|
|
**수정일**: 2025-10-02
|
|
|
|
## 1. 계층 분리 원칙
|
|
|
|
### 필수 계층
|
|
```
|
|
요청 계층 (router/)
|
|
↓
|
|
비즈니스 계층 (services/, llm/, brain/)
|
|
↓
|
|
데이터 계층 (state/, repositories/)
|
|
```
|
|
|
|
### 계층별 책임
|
|
|
|
| 계층 | 역할 | 금지 사항 |
|
|
|------|------|----------|
|
|
| **router/** | HTTP 요청/응답 처리만 | DB 직접 접근, 비즈니스 로직 |
|
|
| **services/llm/** | 비즈니스 로직 구현 | DB 직접 연결 (state를 통해서만) |
|
|
| **state/** | DB CRUD만 | 비즈니스 로직 포함 |
|
|
|
|
## 2. 폴더 구조 규칙
|
|
|
|
### 표준 구조
|
|
```
|
|
{service_name}/
|
|
├── main.py # 앱 실행, 라우터 등록만
|
|
├── app/
|
|
│ ├── router/ # HTTP 엔드포인트
|
|
│ ├── services/ # 비즈니스 로직
|
|
│ ├── state/ # DB 접근
|
|
│ ├── core/ # 설정, 공통 기능
|
|
│ └── utils/ # 유틸리티
|
|
└── tests/
|
|
```
|
|
|
|
### 폴더 명명 규칙
|
|
- `router/` 또는 `api/`: HTTP 처리
|
|
- `services/`: 도메인 로직
|
|
- `state/` 또는 `repositories/`: 데이터 접근
|
|
- 복수형 사용 권장
|
|
|
|
## 3. 파일 명명 규칙
|
|
|
|
### router/
|
|
- `{기능}_handler.py`: 이벤트 처리 (slack_handler.py)
|
|
- `{기능}_endpoint.py`: REST API (emotion_endpoint.py)
|
|
|
|
### services/
|
|
- `{도메인}_{기능}.py`: coldmail_filter.py, ir_analyzer.py
|
|
- 한 파일 최대 500줄
|
|
|
|
### state/
|
|
- `database.py`: 통합 DB 접근
|
|
- `{도메인}_repository.py`: 도메인별 분리 시
|
|
|
|
## 4. 의존성 방향 규칙
|
|
|
|
### 단방향 흐름
|
|
```
|
|
router → services → state
|
|
↓ ↓ ↓
|
|
utils core models
|
|
```
|
|
|
|
### 금지 사항
|
|
- ❌ 순환 참조: A imports B, B imports A
|
|
- ❌ 하위가 상위 호출: state가 services 호출
|
|
- ❌ 계층 건너뛰기: router가 직접 state 호출 (긴급 상황 제외)
|
|
|
|
## 5. 코드 작성 원칙
|
|
|
|
### router 계층
|
|
```python
|
|
# ✅ 올바름
|
|
async def handle_request(data: dict):
|
|
result = await some_service.process(data) # 서비스 호출
|
|
return {"result": result}
|
|
|
|
# ❌ 금지
|
|
async def handle_request(data: dict):
|
|
conn = await asyncpg.connect(...) # DB 직접 접근
|
|
result = complex_logic(data) # 비즈니스 로직
|
|
```
|
|
|
|
### services 계층
|
|
```python
|
|
# ✅ 올바름
|
|
async def process_data(data: dict):
|
|
validated = validate(data) # 비즈니스 로직
|
|
await save_to_db(validated) # state 호출
|
|
return validated
|
|
|
|
# ❌ 금지
|
|
async def process_data(data: dict):
|
|
conn = await asyncpg.connect(...) # 직접 DB 연결
|
|
```
|
|
|
|
### state 계층
|
|
```python
|
|
# ✅ 올바름
|
|
async def save_emotion(data: dict):
|
|
conn = await asyncpg.connect(METRICS_DB_URL)
|
|
await conn.execute("INSERT INTO ...") # DB만
|
|
await conn.close()
|
|
|
|
# ❌ 금지
|
|
async def save_emotion(data: dict):
|
|
if data['emotion'] == 'anger': # 비즈니스 로직
|
|
data = transform(data)
|
|
await conn.execute(...)
|
|
```
|
|
|
|
## 6. DB 접근 규칙
|
|
|
|
### 환경변수 사용
|
|
- `DATABASE_URL`: 메인 DB
|
|
- `METRICS_DATABASE_URL`: 메트릭 전용 DB
|
|
- `TEST_DATABASE_URL`: 테스트 DB
|
|
|
|
### 연결 방식
|
|
```python
|
|
# state/database.py만 DB 연결 가능
|
|
async def get_connection():
|
|
return await asyncpg.connect(os.getenv("DATABASE_URL"))
|
|
```
|
|
|
|
### 금지 사항
|
|
- ❌ router/services에서 직접 asyncpg.connect()
|
|
- ❌ 하드코딩된 DB URL
|
|
- ❌ JSONB 저장 시 dict 직접 전달 (json.dumps() 필수)
|
|
|
|
## 7. 파일 크기 제한
|
|
|
|
- **한 파일 최대 500줄**
|
|
- 초과 시 기능별 분리
|
|
- 예: `services/email_integration.py` (800줄) → `email_send.py` + `email_fetch.py`
|
|
|
|
## 8. Import 규칙
|
|
|
|
### 금지
|
|
```python
|
|
from app.state.database import * # ❌ wildcard
|
|
from ..router.slack_handler import x # ❌ 순환 가능성
|
|
```
|
|
|
|
### 권장
|
|
```python
|
|
from app.state.database import save_emotion_reading # ✅ 명시적
|
|
from app.services import coldmail_filter # ✅ 모듈 import
|
|
```
|
|
|
|
## 9. 체크리스트
|
|
|
|
코드 작성 전:
|
|
- [ ] 이 코드는 어느 계층인가?
|
|
- [ ] DB 접근은 state를 통하는가?
|
|
- [ ] 비즈니스 로직이 router에 있지 않은가?
|
|
- [ ] 순환 import 가능성은 없는가?
|
|
- [ ] 파일 크기가 500줄 이하인가?
|
|
|
|
## 10. 예외 상황
|
|
|
|
### 허용되는 예외
|
|
1. **긴급 핫픽스**: 임시로 계층 건너뛰기 가능 (문서화 필수)
|
|
2. **레거시 코드**: 점진적 리팩토링
|
|
3. **성능 최적화**: 충분한 근거 필요
|
|
|
|
### 예외 처리 시
|
|
```python
|
|
# TODO: 계층 위반 - 리팩토링 필요 (issue #123)
|
|
# 긴급 수정: 2025-10-02, 사유: DB 장애 복구
|
|
```
|