# DB 기반 스케줄러 관리 시스템 구현 **날짜**: 2026-01-02 **작성자**: happybell80 **관련 파일**: `rb8001/app/state/scheduler_repository.py`, `rb8001/app/scheduler/db_loader.py`, `rb8001/app/router/scheduler_endpoint.py`, `rb8001/main.py` --- ## 문제 상황 ### 환경변수 하드코딩 불일치 - `lunch_worldcup.py:16`: 기본값 `"false"` 설정 - `.env` 파일: `LUNCH_WORLDCUP_ENABLED=true` 하드코딩 - 결과: 코드 기본값과 환경변수 불일치로 12시 스케줄 실행됨 - 원인: `311_FastAPI_구조_원칙.md:241-252` 단일 소스 원칙 위반 ### main.py 스케줄 코드 과다 - 732줄 중 130줄이 스케줄 관련 코드 - 환경변수 기반 등록으로 동적 관리 불가 ## 해결 방안 ### Phase 1-2: Repository 구현 - `scheduler_repository.py`: `scheduled_jobs` 테이블 CRUD - `_ensure_table()`: 테이블 자동 생성 - JSONB `config` 필드 파싱: `json.loads()` 적용 (`get_enabled_jobs`, `get_job_by_name`) ### Phase 3: DB 로더 구현 - `db_loader.py`: `JOB_TYPE_MAP`으로 job_type → 함수 매핑 - `load_jobs_from_db()`: DB 조회 → APScheduler 등록 - async 함수는 sync 래퍼(`_run_*_with_logging`) 사용 ### Phase 4: main.py 리팩토링 - `main.py:146-149`: 환경변수 기반 등록 제거, `load_jobs_from_db()` 호출 - `app.state.scheduler` 설정: `scheduler_endpoint.py`에서 `request.app.state.scheduler` 사용 ### Phase 5: API 엔드포인트 - `scheduler_endpoint.py`: CRUD + run 엔드포인트 - `POST /api/scheduler/jobs`: 잡 생성 - `PATCH /api/scheduler/jobs/{name}`: 수정 - `DELETE /api/scheduler/jobs/{name}`: 삭제 - `GET /api/scheduler/jobs`: 목록 조회 ### 테스트 이슈 해결 - JSONB 파싱: `asyncpg`가 JSONB를 string으로 반환 → `json.loads()` 적용 - E2E 테스트 이벤트 루프 충돌: `pytest-asyncio`와 `TestClient` 충돌 - 해결: 동기 `TestClient` fixture + `run_async()` 헬퍼 함수 사용 ## 구현 완료 - 커밋: `1e82dee` (2026-01-02) - 테스트: 13개 모두 통과 (repository 5, loader 3, E2E 5) - 배포: Gitea Actions 자동 배포 완료 ## 교훈 ### 환경변수 관리 원칙 준수 - `os.getenv()` 직접 호출 시 기본값과 `.env` 하드코딩 불일치 위험 - DB 기반 설정으로 단일 소스 원칙 준수 가능 - 향후 Pydantic Settings 전환 권장 (`Phase 7`) ### JSONB 타입 처리 - `asyncpg`는 JSONB를 string으로 반환하므로 명시적 파싱 필요 - `json.loads()` 적용 위치: Repository 레이어에서 일관되게 처리 ### E2E 테스트 이벤트 루프 관리 - `pytest-asyncio`와 FastAPI `TestClient`의 lifespan 이벤트 충돌 가능 - 해결: 동기 `TestClient` + 비동기 작업은 별도 루프에서 실행 - `app.state` 패턴으로 의존성 주입 간소화