96 lines
4.5 KiB
Markdown
96 lines
4.5 KiB
Markdown
# 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`: 목록 조회
|
|
|
|
### Phase 6: 기존 데이터 마이그레이션
|
|
- `scripts/migrate_schedules_to_db.py`: 환경변수 기반 스케줄러를 DB로 마이그레이션
|
|
- 6개 스케줄러 마이그레이션: daily_diary, naverworks_daily, coldmail_daily, lunch_worldcup, daily_headlines, companyx_news
|
|
- 실행: `python -m scripts.migrate_schedules_to_db`
|
|
- 서버 재시작 후 DB에서 5개 enabled 잡 정상 로드 확인
|
|
|
|
### Phase 7: Pydantic Settings 전환
|
|
- `app/core/config.py`: 스케줄러 관련 설정 추가 (하위 호환성 유지)
|
|
- `app/scheduler/jobs/*.py`: `os.getenv()` → `settings` 사용으로 전환
|
|
- `main.py`: daily_headlines, companyx_news 직접 등록 코드 제거, DB 로더로 통합
|
|
- 모든 스케줄러 관련 환경변수를 Pydantic Settings로 통합
|
|
|
|
### 추가 완료: daily_headlines, companyx_news DB 전환
|
|
- `app/scheduler/jobs/daily_headlines.py`, `companyx_news.py`: 래퍼 함수 추가
|
|
- `db_loader.py`: JOB_TYPE_MAP에 daily_headlines, companyx_news 추가, config에서 channel_id 추출 로직 추가
|
|
- `main.py`: daily_headlines, companyx_news 직접 등록 코드 완전 제거
|
|
- 모든 스케줄러(6개) DB 기반 관리로 전환 완료, .env 파일 정리 완료
|
|
|
|
### 테스트 이슈 해결
|
|
- JSONB 파싱: `asyncpg`가 JSONB를 string으로 반환 → `json.loads()` 적용
|
|
- E2E 테스트 이벤트 루프 충돌: `pytest-asyncio`와 `TestClient` 충돌
|
|
- 해결: 동기 `TestClient` fixture + `run_async()` 헬퍼 함수 사용
|
|
|
|
## 구현 완료
|
|
|
|
- Phase 1-5 커밋: `1e82dee` (2026-01-02)
|
|
- Phase 6 커밋: `0d0a098` (2026-01-02)
|
|
- Phase 7 커밋: `f5ac6e9` (2026-01-02)
|
|
- daily_headlines, companyx_news 전환 커밋: `e3226f2` (2026-01-02)
|
|
- news_posting_skill.py 원칙 준수 커밋: `8b54c56` (2026-01-02)
|
|
- 테스트: 13개 모두 통과 (repository 5, loader 3, E2E 5)
|
|
- 마이그레이션: 6개 스케줄러 DB 삽입 완료, 서버 재시작 후 5개 enabled 잡 정상 로드
|
|
- Settings 전환: 모든 스케줄러 관련 환경변수를 Pydantic Settings로 통합 완료
|
|
- .env 정리: 스케줄러 관련 환경변수 13개 주석 처리 완료
|
|
- 배포: Gitea Actions 자동 배포 완료
|
|
|
|
## 교훈
|
|
|
|
### 환경변수 관리 원칙 준수
|
|
- `os.getenv()` 직접 호출 시 기본값과 `.env` 하드코딩 불일치 위험
|
|
- DB 기반 설정으로 단일 소스 원칙 준수 가능
|
|
- Pydantic Settings 전환 완료: 모든 스케줄러 관련 환경변수를 `app/core/config.py`로 통합
|
|
|
|
### JSONB 타입 처리
|
|
- `asyncpg`는 JSONB를 string으로 반환하므로 명시적 파싱 필요
|
|
- `json.loads()` 적용 위치: Repository 레이어에서 일관되게 처리
|
|
|
|
### E2E 테스트 이벤트 루프 관리
|
|
- `pytest-asyncio`와 FastAPI `TestClient`의 lifespan 이벤트 충돌 가능
|
|
- 해결: 동기 `TestClient` + 비동기 작업은 별도 루프에서 실행
|
|
- `app.state` 패턴으로 의존성 주입 간소화
|
|
|