DOCS/journey/troubleshooting/260102_db_scheduler_management.md
Claude-51124 84c122f030 docs: daily_headlines, companyx_news DB 전환 및 .env 정리 내용 반영
- 6개 스케줄러 모두 DB 전환 완료
- .env 파일 정리 완료
- news_posting_skill.py 원칙 준수 반영
2026-01-02 12:45:17 +09:00

4.5 KiB

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-asyncioTestClient 충돌
  • 해결: 동기 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 패턴으로 의존성 주입 간소화