DOCS/book/300_architecture/311_백엔드_구조_원칙.md
Claude-51124 9c2df748b3 docs: 콜드메일 다중 확인 루프 트러블슈팅 및 테스트 원칙 보완
- 트러블슈팅 문서 추가: 260121_coldmail_multi_confirm_loop_fix.md
- 계획 문서 archive 이동: 260121_coldmail_multi_confirm_and_summary_fix.md
- 테스트 원칙 보완: 컨테이너 테스트 파일 반영 방법 (docker cp)
- 백엔드 원칙 보완: 테스트 원칙에 컨테이너 반영 추가
2026-01-21 10:13:06 +09:00

20 KiB

FastAPI 프로젝트 구조 원칙

작성일: 2025-09-17 수정일: 2025-12-26 (LLM 프롬프트 실제 데이터 전달 원칙 추가)

1. 계층 분리 원칙

필수 계층

요청 계층 (router/)
    ↓
비즈니스 계층 (services/, llm/, brain/)
    ↓
데이터 계층 (state/, repositories/)

계층별 책임

계층 역할 금지 사항
router/ HTTP 요청/응답 처리만 DB 직접 접근, 비즈니스 로직
services/ 비즈니스 로직 구현 DB 직접 연결 (state를 통해서만)
state/ DB CRUD만 비즈니스 로직 포함

2. 폴더 구조 규칙

표준 구조

{service_name}/
├── main.py              # 앱 실행, 라우터 등록만
├── app/
│   ├── router/         # HTTP 엔드포인트
│   │   └── v1/         # API 버전 관리 (선택)
│   ├── services/       # 비즈니스 로직
│   ├── state/          # DB 접근 (Repository 패턴)
│   ├── models/         # ORM 모델 (DB 테이블 정의)
│   ├── schemas/        # Pydantic 모델 (API 요청/응답)
│   ├── core/           # 설정, 공통 기능
│   ├── db/             # DB 세션 관리 (선택)
│   └── utils/          # 유틸리티
└── tests/

main.py 역할

  • 앱 실행, 라우터 등록만 담당
  • 엔드포인트 직접 정의 금지 (@app.post(), @app.get() 등)
  • router 파일에서 main.py의 인스턴스(router, scheduler 등) 사용 시 의존성 주입 패턴 사용
    • 예: set_router_instance(), set_scheduler() 함수로 주입
    • router 파일에서 직접 import하지 않음

폴더 명명 규칙

  • router/ 또는 api/: HTTP 처리
  • services/: 비즈니스 로직
  • state/ 또는 repositories/: Repository 패턴으로 CRUD 캡슐화
  • models/: SQLAlchemy 등 ORM 모델
  • schemas/: Pydantic 요청/응답 스키마 (models와 분리)
  • db/: DB 엔진/세션 중앙 관리 (선택)
  • 복수형 사용 권장

3. 파일 명명 규칙

router/

  • {기능}_handler.py: 이벤트 처리 (slack_handler.py)
  • {기능}_endpoint.py: REST API (emotion_endpoint.py)

services/

  • {도메인}_{기능}.py: coldmail_filter.py, ir_analyzer.py
  • 한 파일 최대 300줄

state/ (Repository 패턴)

  • database.py: 통합 DB 접근
  • {도메인}_repository.py: 도메인별 CRUD 캡슐화

utils/

  • {기능}_extractor.py: 데이터 추출 (json_extractor.py)
  • {기능}_helper.py: 도우미 함수
  • 명확하고 구체적인 이름 사용

models/

  • {도메인}_model.py: ORM 모델 (예: user_model.py, emotion_model.py)

schemas/

  • {도메인}_schema.py: API 입출력 스키마 (예: user_schema.py, emotion_schema.py)

4. 의존성 방향 규칙

단방향 흐름

router → services → state/repositories
  ↓         ↓         ↓
schemas   core     models
  ↓
utils

계층 간 데이터 흐름

  • router: schemas로 요청/응답 검증
  • services: schemas + models 사용 가능
  • state/repositories: models만 사용 (DB 접근)

금지 사항

  • 순환 참조: A imports B, B imports A
  • 하위가 상위 호출: state가 services 호출
  • 계층 건너뛰기: router가 직접 state 호출 (긴급 상황 제외)

5. 코드 작성 원칙

중복 코드 제거 우선

  • 동일한 로직이 2개 이상의 위치에 반복되면 utils/에 유틸리티 함수로 추출
  • 한 곳만 수정하여 전체 반영 가능하도록 설계
  • 범용적이고 재사용 가능한 함수로 구현

참고: DOCS/book/300_architecture/guidelines/utility_functions.md

LangGraph 워크플로우

  • 복잡한 다단계 처리: LangGraph 적극 활용
    • 의도 분류, 엔티티 추출, 스킬 선택 등 다단계 워크플로우는 LangGraph로 구현
    • 상태 관리, 조건부 분기, 재시도 로직 등 복잡한 제어 흐름에 적합
  • 프로덕션 핵심 워크플로우: 적절한 체크포인터(AsyncSqliteSaver, PostgresSaver 등)로 상태 영속성 구현
    • 장기 실행 워크플로우는 상태 영속성 필수
    • 중단 시 복구 가능하도록 체크포인트 저장
  • 실험/경량 플로우: stateless LangGraph 허용
    • 단순한 워크플로우는 stateless로 시작, 필요 시 체크포인트 추가
  • 워크플로우 중복 실행 방지: LangGraph 워크플로우가 활성화되면 개별 Phase 단계는 건너뛰기
    • 워크플로우 내부에서 이미 처리하는 단계(질문 확장, 의도 분류 등)를 외부에서 중복 실행하지 않음
    • 예: INTENT_USE_LANGGRAPH=true이면 message_service의 Phase 1(질문 확장) 건너뛰기

계층별 원칙

  • router: 서비스 호출만, DB/비즈니스 로직 금지
  • services: 비즈니스 로직 구현, state를 통한 DB 접근
  • state: DB CRUD만, 비즈니스 로직 금지

6. DB 접근 규칙

환경변수 사용

  • DATABASE_URL: 메인 DB
  • METRICS_DATABASE_URL: 메트릭 전용 DB
  • TEST_DATABASE_URL: 테스트 DB

연결 방식

  • 권장: db/database.py에서 DB 세션 중앙 관리 (의존성 주입)
  • router 파일에서 main.py 인스턴스 사용: set_router_instance(), set_scheduler() 같은 함수로 주입
    • 예: system_endpoint.py에서 router 인스턴스, schedule_endpoint.py에서 scheduler 인스턴스
  • 간단한 경우: state/database.py에서 직접 연결

Supabase 쿼리 컬럼명 충돌 처리

  • 예약어/컬럼명 충돌: order 같은 예약어가 컬럼명과 겹칠 경우 쿼리 체이닝에서 직접 .order() 사용 금지
  • Python 로직 처리: 컬럼명 충돌 시 Supabase 쿼리 체이닝 대신 Python 로직으로 정렬 처리 (모든 데이터 조회 후 Python에서 정렬)
  • 별칭 사용: 가능한 경우 컬럼명 별칭 사용하여 충돌 회피

더미/목 데이터 금지 원칙

  • 절대 금지: 프로덕션 환경에서 더미 데이터(mock data, dummy data)를 생성하거나 사용하지 않음
  • 데이터 수집 필수: 실제 데이터 소스(공식 API, 공개 데이터, 수집 파이프라인)에서 데이터를 수집해야 함
  • 임시 데이터: 개발/테스트 환경에서만 더미 데이터 허용 (명시적으로 if os.getenv("ENVIRONMENT") == "development" 조건부 사용)
  • 데이터 부재 시: 데이터가 없는 경우 기능을 비활성화하거나 "데이터 수집 중" 메시지 표시, 절대 랜덤/더미 데이터로 대체하지 않음

금지 사항

  • 프로덕션 router/services에서 직접 asyncpg.connect()
  • 하드코딩된 DB URL
  • JSONB 저장 시 dict 직접 전달 (json.dumps() 필수)
  • 프로덕션 요청 경로에서 직접 DB 연결 재사용
  • Supabase 쿼리 체이닝에서 컬럼명과 예약어 충돌 시 직접 사용 (Python 로직으로 처리)
  • 프로덕션에서 더미/목 데이터 생성 및 사용 (random, mock, dummy 데이터 생성 금지)

6-1. DB 스키마 변경 시 동기화 필수

핵심 원칙: ORM 모델, DDL, Repository 코드를 동시에 수정해야 함

필수 동기화 항목

  1. ORM 모델 (state/{도메인}_repository.py 또는 models/{도메인}_model.py)
    • 컬럼 타입, nullable 여부, 기본값 등
  2. DDL (_ensure_tables() 또는 마이그레이션 스크립트)
    • CREATE TABLE, ALTER TABLE 문
  3. Repository 코드 (state/{도메인}_repository.py)
    • INSERT/UPDATE 시 필드 처리 로직

체크리스트

  • ORM 모델 수정 완료
  • DDL 수정 완료 (기존 DB 마이그레이션 스크립트 작성)
  • Repository 코드 수정 완료 (.get() 사용 등)
  • 테스트 작성 및 검증 완료

교훈

  • 한 곳만 수정 시 런타임 에러(KeyError 등) 또는 스키마 불일치 발생
  • 스키마 변경 시 3곳(ORM/DDL/Repository) 동시 점검 필수

7. 파일 크기 제한

  • 한 파일 최대 300줄 권장
  • 초과 시 기능별 분리

8. Import 규칙

금지

  • wildcard import (from module import *)
  • 상대 import로 순환 참조 가능성 (from ..router import x)

권장

  • 명시적 import (from app.state.database import save_emotion_reading)
  • 모듈 import (from app.services import coldmail_filter)

9. 체크리스트

코드 작성 전:

  • 이 코드는 어느 계층인가?
  • DB 접근은 state를 통하는가?
  • 비즈니스 로직이 router에 있지 않은가?
  • 순환 import 가능성은 없는가?
  • 핵심 파일은 300줄 이하로 유지할 수 있는가?
  • 중복 코드는 utils/로 추출할 수 있는가?
  • DB 스키마 변경 시 ORM/DDL/Repository 동시 수정 확인
  • Supabase 쿼리에서 컬럼명과 예약어 충돌 시 Python 로직으로 처리했는가?
  • LLM 호출 횟수 계산 및 최적화 검토
  • 애매한 케이스는 LLM 우선 접근 원칙 적용 확인
  • 복잡한 워크플로우는 LangGraph 활용 검토
  • 원칙 문서 확인 완료 (311_FastAPI_구조_원칙.md, 312_문서_작성_원칙.md)

배포 전/후 확인:

  • 코드 변경 후 git status로 커밋되지 않은 변경사항 확인
  • 프론트엔드/백엔드 모두 배포 완료 확인
  • 배포 후 실제 브라우저에서 동작 확인 (추측하지 말고 직접 확인)
  • 백엔드 로그에서 에러 확인 (docker logs 또는 Supabase 로그)

10. 예외 상황

허용되는 예외

  1. 긴급 핫픽스: 임시로 계층 건너뛰기 가능 (문서화 필수)
  2. 레거시 코드: 점진적 리팩토링
  3. 성능 최적화: 충분한 근거 필요

예외 처리 시

  • TODO 주석으로 계층 위반 표시
  • 긴급 수정 사유 명시

11. 로깅 원칙

로그 레벨 사용 기준 (Python logging 공식 문서):

  • DEBUG: 개발/디버깅용 상세 정보 (중간 과정, 내부 상태)
  • INFO: 정상 동작 및 주요 이벤트 (시작/종료, 주요 단계)
  • WARNING: 잠재적 문제 (예상치 못한 상황, 성능 저하 가능성)
  • ERROR: 오류 발생 (기능 실패, 예외 처리)

규칙:

  • 시작/종료는 반드시 INFO 레벨
  • 중간 과정은 DEBUG 레벨
  • 프로덕션에서는 INFO 기본, DEBUG는 필요 시에만 활성화

12. 환경변수 관리 원칙

단일 소스 원칙:

  • .env: 모든 환경변수 값의 단일 소스 (실제 값만 저장)
  • docker-compose.yml: env_file: - .env로 자동 로드, environment: 섹션은 선택사항
  • config.py: Pydantic Settings로 .env 자동 로드, 타입 검증 및 기본값만 담당

금지 사항:

  • .env, docker-compose.yml, config.py에 동일한 변수를 중복 정의
  • 코드에서 os.getenv() 직접 호출 (Pydantic Settings 사용)
  • docker-compose.ymlenvironment: 섹션에 하드코딩된 값
  • 코드에 API 키, 토큰, 비밀번호 등 민감 정보를 기본값/백업 값으로 직접 하드코딩

마이그레이션 시 주의사항:

  • DB 마이그레이션 시 .env 정리 필수: 환경변수 기반 설정을 DB로 옮긴 후, .env에서 관련 변수 주석 처리 또는 삭제 필요 (마이그레이션 스크립트 실행 후 누락 방지)
  • 환경변수 사용 여부는 코드에서 확인: config.py에 정의되어 있어도 실제 사용 여부는 grep 등으로 코드 검색하여 확인 필요 (불필요한 변수 유지 방지)
  • 동적 관리가 필요한 설정은 DB 사용: 스케줄러처럼 런타임 변경이 필요한 설정은 DB로 관리하는 것이 적절함 (환경변수 재시작 필요 문제 해결)

13. LLM 우선 접근 원칙

핵심 원칙: LLM을 기본으로 사용하고, 하드코딩된 규칙(heuristic)은 최소화

LLM 기본 사용

  1. 의도 분류: LLM이 기본, 규칙은 FastPath 최적화용으로만 사용
    • 짧은 질문("어디서?", "결과는?")은 LLM이 맥락을 해석하여 확장
    • 맥락 의존 질문("그거 어떻게 됐어?", "취소해줘")은 LLM이 선행사 해석
  2. 엔티티 추출 및 해석: LLM이 기본, 규칙은 보조
    • 대명사, 지시어 해소는 LLM 활용
    • 명확한 패턴(날짜, 시간 등)만 규칙으로 빠르게 처리
  3. 모호한 표현 처리: LLM이 컨텍스트를 이해하여 적절한 응답 생성
  4. 복잡한 워크플로우: LangGraph로 LLM 기반 다단계 처리

규칙 기반은 성능 최적화용으로만 제한

  • FastPath 최적화: 인사("안녕"), 명령어("/news") 등 매우 명확한 패턴만 규칙 사용
  • 성능/비용 최적화: 확신도 매우 높은 케이스(> 0.95)만 규칙으로 빠르게 처리
  • 규칙 추가 금지: 새로운 패턴마다 규칙 추가하지 말고 LLM 활용

금지 사항

  • LLM 호출을 피하기 위해 규칙/패턴 매칭으로 처리
  • 복잡한 규칙 체인 구축 (LLM이 더 정확하고 유지보수 용이)
  • 새로운 패턴마다 규칙 추가하는 방식 (LLM이 자동으로 처리)
  • "규칙으로 처리 가능하면 규칙 사용" 사고방식 (LLM 우선 사고)

장점

  • 유연성: 새로운 패턴에 대한 규칙 추가 없이 LLM이 자연스럽게 처리
  • 정확도: 맥락 이해 능력으로 규칙 기반보다 정확한 해석
  • 유지보수: 규칙 관리 부담 감소, LLM이 자동으로 패턴 학습

14. LLM 호출 최적화 원칙

핵심 원칙: 호출 횟수 계산 및 최적화 사전 검토 필수

필수 검토 사항

  1. 호출 횟수 계산: 페이지/문서당 LLM 호출 횟수 사전 계산
  2. API 할당량 확인: 사용하는 LLM API의 할당량 제한 확인 (RPM, RPD 등)
  3. 통합 가능 여부: 단일 프롬프트로 통합 가능한 작업은 반드시 통합

최적화 방법

  • 단일 호출 통합: 여러 개별 호출을 하나의 프롬프트로 통합
  • 배치 처리: 가능한 경우 여러 항목을 한 번에 처리
  • 캐싱: 동일한 입력에 대한 결과 캐싱
  • 프롬프트는 실제 데이터 전달: 요약 문자열 대신 실제 데이터(대화/활동/감정 상세 등)를 전달해야 LLM이 구체적 결과 생성 가능

체크리스트

  • LLM 호출 횟수 계산 완료
  • API 할당량 제한 확인 완료
  • 통합 가능한 호출 통합 완료
  • 테스트로 호출 횟수 검증 완료

교훈

  • 호출 횟수 미검토 시 API 할당량 초과(429 에러) 발생 가능
  • 단일 프롬프트로 통합 가능한 작업은 반드시 통합하여 호출 횟수 최소화

15. 장기 작업(LLM/RAG 등) 처리 원칙

핵심 원칙: HTTP 요청으로 노출되는 장기 작업(LLM, RAG, 대용량 PDF 처리 등)은 동기식으로 붙잡지 말고, 항상 "즉시 ID 반환 + 비동기 백그라운드 작업 + 후속 조회/웹훅" 패턴으로 설계한다.

권장 패턴

  • POST /api/.../start → 백그라운드 작업 등록, 즉시 job_id 또는 evaluation_id 반환
  • GET /api/.../{id} → 상태/결과 조회 (프론트는 폴링 또는 SSE/WebSocket 사용)
  • 필요 시 웹훅/이벤트로 완료 알림 (Slack, 이메일 등)

금지 사항

  • Nginx proxy_read_timeout에 의존해 60초 이상 동기식으로 응답을 붙잡는 설계
  • LLM/RAG/외부 API 호출이 여러 단계로 이어지는 워크플로우를 단일 HTTP 요청에 모두 묶는 방식

Nginx 타임아웃 설정 확인 필수

핵심 원칙: 장기 작업을 위한 프록시 엔드포인트는 HTTP/HTTPS 블록 모두에 충분한 타임아웃 설정 필요

필수 확인 사항:

  • Nginx 설정 파일에서 프록시 경로의 location 블록 확인
  • proxy_read_timeout, proxy_connect_timeout, proxy_send_timeout 설정 확인
  • HTTP(port 80)와 HTTPS(port 443) 블록 모두에 동일한 타임아웃 설정 적용
  • 프록시 타임아웃은 백엔드 작업 시간보다 충분히 길게 설정

16. 정적 파일 서빙 원칙

필수 원칙

  • 백엔드 금지: FastAPI 백엔드는 정적 파일(HTML/CSS/JS)을 서빙하지 않음
  • nginx 직접 서빙: nginx가 정적 파일을 직접 서빙하는 업계 표준 방식 사용
  • 역할 분리: 백엔드는 API 처리에 집중, 웹서버(nginx)는 정적 파일 서빙 담당
  • FileResponse 제거: 백엔드에서 FileResponse로 정적 파일 서빙하는 코드 작성 금지

배경

  • nginx가 정적 파일을 직접 서빙하면 FastAPI 프로세스를 거치지 않아 성능 최적화
  • 프로덕션 환경에서 표준적인 배포 방식
  • 백엔드와 웹서버의 명확한 역할 분리

17. Slack API 호출 원칙

  • skill-slack API 사용 필수: rb8001에서 Slack API 호출 시 WebClient 직접 사용 금지, skill-slack HTTP API 사용
  • thread_ts 처리: None/빈 문자열일 때는 payload에 포함하지 않음 (조건부 포함 필수)
  • 메시지 업데이트: 인터랙티브 버튼 응답은 원본 메시지 업데이트(/api/v1/update) 사용

18. 테스트 원칙

  • 실제 테스트 필수: 코드 수정 후 추측하지 말고 실제로 테스트 (curl, Slack 직접 사용, DB 조회)
  • 컨테이너 테스트 파일 반영: tests 디렉토리는 볼륨 마운트 아님 (빌드 시 COPY). 새 테스트 파일 추가 시 docker cp [파일경로] [컨테이너명]:/code/tests/ 사용 또는 컨테이너 재빌드

19. 리팩토링 시 로직 상실 방지 원칙

필수 체크리스트 (코드 이동/리팩토링 전)

1. 의존성 추적

  • 이동할 함수/메서드의 모든 호출부 찾기 (grep -r "함수명")
  • 이동할 함수 내부의 모든 함수 호출/DB 저장/로깅 등 부수 효과(side effect) 목록화
  • 이동 전 코드의 전체 실행 흐름 문서화 (호출 → 처리 → 저장 → 응답)

2. 기능 동일성 검증

  • 이동 전 코드의 모든 기능을 테스트로 검증 (DB 저장, 로깅, 에러 처리 등)
  • 이동 후 동일한 테스트로 재검증 (기능 누락 확인)
  • 실제 환경에서 E2E 테스트 수행 (단위 테스트만으로는 부족)

3. 코드 이동 절차

  • 이동할 코드 블록 전체 복사 (주석, 로깅, 에러 처리 포함)
  • 새 위치에 붙여넣기 후 import 경로 수정
  • 모든 호출부를 새 위치로 변경
  • 기존 코드 제거 전 테스트 통과 확인
  • 기존 코드 제거 후 재테스트

4. 부수 효과(side effect) 확인

  • DB 저장 로직 (save_*, insert, update 등)
  • 로깅 (logger.info, logger.error 등)
  • 캐시 업데이트
  • 외부 API 호출
  • 이벤트 발행/구독
  • 상태 변경

금지 사항

  • 기능만 이동하고 부수 효과 제거: 감정 분석만 이동하고 DB 저장 로직 누락
  • 호출부 일부만 수정: 일부 호출부는 새 위치, 일부는 기존 위치 혼재
  • 테스트 없이 리팩토링: "동작할 것 같다"는 추측으로 진행
  • 점진적 이동: 일부만 이동하고 나머지는 나중에 → 누락 위험

권장 사항

  • 전체 이동 후 테스트: 코드를 한 번에 모두 이동하고 테스트
  • 리팩토링 전후 비교 문서화: 이동 전 코드와 이동 후 코드를 문서에 기록
  • 의존성 그래프 작성: 이동할 함수가 호출하는 모든 함수와 호출하는 모든 함수를 시각화
  • E2E 테스트 필수: 단위 테스트뿐만 아니라 실제 사용 시나리오 테스트

교훈 (실제 사례)

사례 1: emotion_readings 저장 로직 누락 (2025-12-23)

  • 문제: router._call_internal_llminternal_llm_service.py로 이동 시 save_emotion_reading() 호출 누락
  • 원인: 감정 분석 로직만 이동하고 DB 저장 부수 효과 미이동
  • 해결: 리팩토링 체크리스트 적용하여 부수 효과 확인 후 이동

사례 2: 동일 문제 재발 (2025-10-02)

  • 문제: 동일한 save_emotion_reading() 누락 문제 재발
  • 원인: 리팩토링 체크리스트 없이 진행
  • 교훈: 체크리스트 문서화 및 필수 준수 필요
  • Git 커밋 확인: 각 프로젝트 폴더에서 개별 확인 (루트에서 확인 금지)

19. 모범 사례 참고

본 문서는 FastAPI 커뮤니티의 다음 모범 사례를 반영하였습니다:

  1. models/schemas 분리: DB 스키마와 API 스펙 독립 관리
  2. Repository 패턴: state/repositories에서 CRUD 캡슐화
  3. DB 세션 중앙화: db/database.py에서 의존성 주입
  4. API 버전 관리: router/v1/, router/v2/ 구조
  5. 관심사 분리: 요청/비즈니스/데이터 계층 명확한 역할 분담