# Backend Coding Principles

**작성일**: 2025-09-17
**수정일**: 2026-01-21 (파일 크기 제한 300줄 → 500줄 완화)
**상위 원칙**: [0_VALUE Coding Principles](../../../../0_VALUE/02_Governance/coding-principles.md)

## 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하지 않음
- **`당장 동작` 우선 구현 절대 금지**
  - 운영 중인 `main.py`에 스케줄/복구/외부 호출/무거운 초기화를 임시로 누적해 문제를 덮는 방식 금지
  - 임시 우회/폴백으로 증상을 숨기는 구현 금지
  - 위반 발견 시 기능 추가보다 책임 분리 리팩토링을 우선 수행

### 폴더 명명 규칙
- `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
- 한 파일 최대 500줄

### 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(질문 확장) 건너뛰기
- **임시 플래그 장기 방치 금지**: 전환용 기본값/우회 플래그는 배포 안정화 기간 종료 시 제거 또는 강제 전환한다.
  - 예: `INTENT_ENGINE` 기본값을 구버전(`v1`)으로 장기 유지하는 구성 금지
  - 예외가 필요한 경우 문서에 종료 조건(기한, 책임자, 제거 PR)을 함께 기록

### 계층별 원칙
- **router**: 서비스 호출만, DB/비즈니스 로직 금지
- **services**: 비즈니스 로직 구현, state를 통한 DB 접근
- **state**: DB CRUD만, 비즈니스 로직 금지

### 예외 처리/웹훅 원칙
- **HTTPException 재포장 금지**: `except Exception`에서 `HTTPException`까지 500으로 감싸지 않는다. `except HTTPException: raise`를 기본 패턴으로 둔다.
- **Slack 서명 검증 경로 원문 보존**: 프록시 경유 웹훅은 원문 body를 유지해야 하며, 서명 검증 경로에서 body 재직렬화를 금지한다.
- **원인 직접 수정 우선**: 증상을 감추는 광범위 예외 폴백/임시 우회 코드를 금지한다. 재현 가능한 근본 원인(입력/경로/설정/데이터)을 먼저 수정한다.
- **상태코드 의미 보존**: 인증 실패(401/403), 권한 문제(403), 입력 오류(400)는 원래 의미대로 반환하고 500으로 왜곡하지 않는다.
- **폴백 남용 금지**: 폴백은 사용자 보호용 최후 수단으로만 사용한다. 개선 신호를 가리는 무조건 폴백(`catch-all fallback`)을 금지한다.
- **실패와 0건 결과 혼동 금지**: 외부 조회 실패, timeout, refresh 실패를 `[]`, `None`, `"no data"` 같은 정상 결과로 치환하지 않는다. "실패"와 "실제 데이터 없음"은 서로 다른 타입과 로그로 분리한다.
- **스케줄러 성공 오판 금지**: 배치/스케줄러 래퍼는 하위 실패를 삼키지 않고 다시 올린다. 운영 로그의 success는 실제 성공과 같아야 한다.
- **관측 가능한 실패 보장**: 운영/카나리 경로에는 `strict_observe` 모드를 두어 일부 요청은 폴백 없이 실패를 노출하고, 구조화된 오류 로그/메트릭을 반드시 남긴다.
- **로그 없는 폴백 금지**: 폴백이 발생하면 `request_id`, `route`, `template/version`, `fallback_reason`, `upstream_error`를 필수 기록한다.

## 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 스키마 확인 및 변경 시 동기화 필수

**핵심 원칙**: 구현 전 실제 DB 스키마 확인 필수, ORM 모델, DDL, Repository 코드를 동시에 수정해야 함

### 구현 전 필수 확인 사항
1. **실제 DB 스키마 확인**: `information_schema.columns` 조회 또는 DDL 스크립트 확인
   - 계획 문서의 스키마와 실제 DB 스키마 불일치 가능성 있음
   - 컬럼 존재 여부, 타입, nullable 여부 확인
2. **기존 마이그레이션 스크립트 확인**: `scripts/` 디렉토리의 DDL 파일 확인
   - 최소 버전(minimal) 스크립트와 전체 버전 스크립트 차이 확인
   - 실제 적용된 스크립트 확인

### 필수 동기화 항목
1. **ORM 모델** (`state/{도메인}_repository.py` 또는 `models/{도메인}_model.py`)
   - 컬럼 타입, nullable 여부, 기본값 등
2. **DDL** (`_ensure_tables()` 또는 마이그레이션 스크립트)
   - CREATE TABLE, ALTER TABLE 문
3. **Repository 코드** (`state/{도메인}_repository.py`)
   - INSERT/UPDATE 시 필드 처리 로직
   - SELECT 쿼리에서 실제 존재하는 컬럼만 조회

### 체크리스트
- [ ] **구현 전 실제 DB 스키마 확인 완료** (information_schema 또는 DDL 스크립트)
- [ ] ORM 모델 수정 완료
- [ ] DDL 수정 완료 (기존 DB 마이그레이션 스크립트 작성)
- [ ] Repository 코드 수정 완료 (실제 존재하는 컬럼만 사용)
- [ ] 테스트 작성 및 검증 완료

### 교훈
- 구현 전 스키마 확인 없이 계획 문서 기반으로 쿼리 작성 시 컬럼 없음 에러 발생
- 한 곳만 수정 시 런타임 에러(KeyError 등) 또는 스키마 불일치 발생
- 스키마 변경 시 3곳(ORM/DDL/Repository) 동시 점검 필수

## 7. 파일 크기 제한

- **한 파일 최대 500줄 권장**
- 초과 시 기능별 분리

## 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 가능성은 없는가?
- [ ] 핵심 파일은 500줄 이하로 유지할 수 있는가?
- [ ] 중복 코드는 utils/로 추출할 수 있는가?
- [ ] **DB 관련 작업 시 실제 DB 스키마 확인 완료** (구현 전 필수)
- [ ] DB 스키마 변경 시 ORM/DDL/Repository 동시 수정 확인
- [ ] Supabase 쿼리에서 컬럼명과 예약어 충돌 시 Python 로직으로 처리했는가?
- [ ] LLM 호출 횟수 계산 및 최적화 검토
- [ ] 애매한 케이스는 LLM 우선 접근 원칙 적용 확인
- [ ] 복잡한 워크플로우는 LangGraph 활용 검토
- [ ] 원칙 문서 확인 완료 (`311_FastAPI_구조_원칙.md`, `312_writing-principles.md`)
- [ ] 웹훅 프록시 경로에서 원문 body 전달(`content=raw_body`) 여부 확인
- [ ] 인증 실패 상태코드(403/401)가 500으로 재매핑되지 않는지 확인
- [ ] 증상 완화용 폴백 대신 근본 원인 수정이 반영되었는지 확인

배포 전/후 확인:
- [ ] 코드 변경 후 `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.yml`의 `environment:` 섹션에 하드코딩된 값
- ❌ 코드에 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 호출을 피하기 위해 규칙/패턴 매칭으로 처리
- ❌ 오타, 별칭, 축약, 복합 발화, 후속 질문 가능성이 있는 도메인을 키워드 몇 개로 앞단에서 잘라내는 방식
- ❌ 키워드 불일치만으로 `UNKNOWN -> 일반 chat`으로 떨어뜨리고, 근거 탐색 가능성을 잃는 방식
- ❌ 의도 분석을 너무 얕고 빠르게 끝내서 실제 도메인 질의를 일반 대화나 추정 응답으로 보내는 방식

### 운영 해석 원칙
1. 엔티티/도메인 질의는 `정규화 -> 엔티티 해석 -> 필요시 근거 탐색 경로 진입`을 먼저 본다.
2. FastPath 규칙은 명확한 인사/명령어/확정 패턴에만 쓴다.
3. 애매한 질의는 `값싼 규칙으로 종료`하지 말고 더 깊은 해석 경로로 넘긴다.
4. 운영상 중요한 질의에서 앞단 규칙 실패는 `일반 chat 허용` 근거가 아니라 `정규화/해석 경로 부족` 신호로 본다.
- ❌ 복잡한 규칙 체인 구축 (LLM이 더 정확하고 유지보수 용이)
- ❌ 새로운 패턴마다 규칙 추가하는 방식 (LLM이 자동으로 처리)
- ❌ "규칙으로 처리 가능하면 규칙 사용" 사고방식 (LLM 우선 사고)
- ❌ 질문별 `if` 분기와 질문별 프롬프트/답변 문장을 코드에 누적해 시나리오를 닫는 방식
- ❌ 대표 질문 몇 개만 통과시키는 특례 처리로 `완료` 판단하는 방식
- ❌ `검색 hit 있음 = 성공`처럼 취급하고 질문 적합도/근거 부족 판정을 생략하는 방식

### 추가 강제 규칙
- LLM/RAG 응답 경로는 `질문 유형 계약`, `근거 채택 규칙`, `근거 부족 시 실패 규칙`을 공통 구조로 먼저 정의해야 합니다.
- 질문이 늘어날수록 분기와 프롬프트가 늘어나는 구조는 설계 실패로 간주합니다.
- 특정 질문 대응이 필요하면 질문별 특례를 추가하기 전에 공통 계약으로 올릴 수 없는지 먼저 검토합니다.

### 장점
- **유연성**: 새로운 패턴에 대한 규칙 추가 없이 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. 테스트 원칙

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

## 18-1. 변경 실행 워크플로우

**핵심 원칙**: 변경은 `계획 -> 구현 -> 테스트 -> push -> 배포 대기 -> 실제 검증 -> worklog` 순서를 기본으로 고정합니다.

### 기본 순서
1. **계획 확인**
- 열린 `troubleshooting/scenario`와 대응 `research/plans`를 먼저 확인합니다.
- 이번 변경이 어떤 문서를 닫기 위한 실행인지 먼저 고정합니다.

2. **구현**
- 계획 범위 안에서만 수정합니다.
- 질문별 특례 하드코딩, 광범위 폴백, 임시 우회로 닫지 않습니다.

3. **테스트**
- 관련 단위 테스트, 통합 테스트, 최소 재현 질문셋을 먼저 돌립니다.
- 가능하면 실패 케이스도 함께 검증합니다.

4. **push**
- 자동 배포 레포는 `git push origin main`을 실행합니다.
- 수동 배포 레포는 해당 서비스 배포 절차를 따릅니다.

5. **배포 대기**
- Actions/배포 파이프라인 완료를 기다립니다.
- 성공 메시지만으로 완료 판단하지 않습니다.

6. **실제 검증**
- `docker ps`, 헬스체크, 실제 API 응답, 실제 로그를 확인합니다.
- 이번 변경의 재현 질문셋/시나리오 기준 질문을 운영 경로에서 다시 확인합니다.

7. **문서 마감**
- 문제 없이 끝났으면 `worklog`를 남깁니다.
- 실행 중 새 문제가 생기면 `worklog`가 아니라 `troubleshooting`으로 되돌립니다.
- `plans`는 완료 보고서로 길게 덮어쓰지 않고, 상태와 연결 문서만 갱신합니다.

### 완료 금지 기준
- 테스트 전 `push`하고 끝냈다고 말하지 않습니다.
- 배포 완료 메시지만 보고 끝냈다고 말하지 않습니다.
- 실제 재현 질문/실제 응답/실제 로그 확인 없이 `해결`이라고 말하지 않습니다.
- 문제가 남아 있으면 `worklog`로 보내지 않습니다.

## 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_llm`을 `internal_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. **관심사 분리**: 요청/비즈니스/데이터 계층 명확한 역할 분담
