DOCS/journey/plans/260102_db_scheduler_management.md

7.2 KiB

DB 기반 동적 스케줄러 관리 시스템

날짜: 2026-01-02 작성자: happybell80 관련 서비스: rb8001 상태: 계획


목적

main.py 732줄 중 130줄이 스케줄 관련 코드. DB로 이동하여 동적 관리.

현재 스케줄러 현황

스케줄러 위치 환경변수 기본값 상태
naverworks_briefing app/scheduler/jobs/naverworks_briefing.py NAVERWORKS_BRIEFING_ENABLED false 환경변수 기반
coldmail_briefing app/scheduler/jobs/coldmail_briefing.py COLDMAIL_BRIEFING_ENABLED false 환경변수 기반
lunch_worldcup app/scheduler/jobs/lunch_worldcup.py LUNCH_WORLDCUP_ENABLED false 12시 스케줄, 비활성화 예정
diary_generator app/scheduler/jobs/diary_generator.py DIARY_GENERATOR_ENABLED true 환경변수 기반
daily_headlines main.py:164-205 HEADLINES_SCHEDULE_ENABLED true main.py 직접 구현
companyx_news main.py:208-248 COMPANY_X_NEWS_ENABLED true main.py 직접 구현
scheduler_status_check main.py:146-162 없음 항상 활성 main.py 직접 구현

테이블 구조

CREATE TABLE IF NOT EXISTS scheduled_jobs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(100) UNIQUE NOT NULL,
    job_type VARCHAR(50) NOT NULL,
    cron_expression VARCHAR(50) NOT NULL,
    enabled BOOLEAN DEFAULT true,
    config JSONB DEFAULT '{}',
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_enabled ON scheduled_jobs(enabled);
CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_job_type ON scheduled_jobs(job_type);

Job Type 매핑

job_type 함수 경로 비고
naverworks_briefing app.services.skills.naverworks_briefing.NanerWorksBriefingSkill.process_briefing async 함수
coldmail_briefing app.scheduler.jobs.coldmail_briefing._run_coldmail_briefing async 함수
diary_generator app.scheduler.jobs.diary_generator._run_diary_generator_with_logging sync 래퍼
lunch_worldcup app.notifications.send_lunch_worldcup_notification async 함수
companyx_news app.services.skills.news_posting_skill.NewsPostingSkill.process_news_batch async 함수
daily_headlines app.services.skills.startup_news_skill.run_headlines_job async 함수

마이그레이션 단계

Phase 1: 테이블 및 Repository 생성

  • app/state/scheduler_repository.py 생성
  • _ensure_table() 함수로 테이블 생성 (다른 repository 패턴 참고)
  • CRUD 함수 구현: get_enabled_jobs(), create_job(), update_job(), delete_job()

Phase 2: 12시 스케줄 비활성화 (즉시)

  • lunch_worldcup.py:16 기본값 확인: 이미 "false"이므로 실행 안됨
  • DB 마이그레이션 전 명시적 비활성화: 환경변수 설정 또는 코드에서 주석 처리

Phase 3: DB 기반 스케줄러 로더 구현

  • app/scheduler/db_loader.py 생성
  • load_jobs_from_db(scheduler) 함수: DB 조회 → job_type 매핑 → APScheduler 등록
  • async 함수 처리: asyncio.run() 또는 래퍼 함수 사용

Phase 4: main.py 리팩토링

  • 기존 환경변수 기반 등록 코드 제거 (131-144줄)
  • db_loader.load_jobs_from_db(scheduler) 호출로 대체
  • 헤드라인/뉴스 스케줄도 DB 기반으로 전환

Phase 5: API 엔드포인트 구현

  • app/router/scheduler_endpoint.py 생성 (기존 schedule_endpoint.py와 분리)
  • POST /api/scheduler/jobs - 새 잡 추가
  • PATCH /api/scheduler/jobs/{name} - 스케줄 수정 (enabled, cron_expression, config)
  • DELETE /api/scheduler/jobs/{name} - 잡 삭제
  • GET /api/scheduler/jobs - 목록 조회

Phase 6: 기존 데이터 마이그레이션

  • 환경변수 기반 스케줄러를 DB에 초기 데이터로 삽입
  • 마이그레이션 스크립트: scripts/migrate_schedules_to_db.py

Phase 7: 환경변수 관리 원칙 준수 (선택, 권장)

  • os.getenv() 직접 호출 제거: app/scheduler/jobs/*.py에서 DB 조회로 대체
  • Pydantic Settings 전환: app/core/config.py에 스케줄러 관련 설정 추가 (하위 호환성 유지)
  • .env 파일 정리: 스케줄러 관련 환경변수 제거 또는 주석 처리
  • 참고: 311_FastAPI_구조_원칙.md:241-252 환경변수 관리 원칙

동작 방식

  1. 서버 시작 시 startup_event()에서 db_loader.load_jobs_from_db(scheduler) 호출
  2. DB에서 enabled=true 조회
  3. job_type으로 함수 매핑 (매핑 테이블 사용)
  4. APScheduler에 등록
  5. 런타임 중 API로 추가/수정/삭제 시 즉시 반영 (scheduler.add_job/remove_job)

API 엔드포인트 상세

POST /api/scheduler/jobs

{
  "name": "lunch_worldcup",
  "job_type": "lunch_worldcup",
  "cron_expression": "0 12 * * mon-fri",
  "enabled": false,
  "config": {"channel_id": "C09CP4MDX71"}
}

PATCH /api/scheduler/jobs/{name}

{
  "enabled": true,
  "cron_expression": "0 13 * * mon-fri",
  "config": {"channel_id": "C09CP4MDX71"}
}

초기 데이터 (마이그레이션용)

name job_type cron_expression enabled config
naverworks_daily naverworks_briefing 0 9 * * mon-fri false {}
coldmail_daily coldmail_briefing 5 9 * * mon-fri false {}
lunch_worldcup lunch_worldcup 0 12 * * mon-fri false {"channel_id": "C09CP4MDX71"}
daily_diary diary_generator 0 2 * * * true {}
daily_headlines daily_headlines 0 9 * * * true {"channel_id": "환경변수에서"}
companyx_news companyx_news 0 10 * * mon-fri true {"channel_id": "C09CP4MDX71"}

환경변수 하드코딩 문제 해결

현재 문제점

  • 이중 정의: 코드 기본값(os.getenv("LUNCH_WORLDCUP_ENABLED", "false"))과 .env 파일 하드코딩(LUNCH_WORLDCUP_ENABLED=true) 불일치
  • 원칙 위반: 311_FastAPI_구조_원칙.md:241-252의 "단일 소스 원칙" 위반 - 코드에서 os.getenv() 직접 호출, .env에 하드코딩
  • 설정 관리 체계 부재: Pydantic Settings 미사용으로 환경변수 관리 일관성 없음

DB 마이그레이션으로의 해결

  • 스케줄러 관련 환경변수 하드코딩 문제 해결: DB에서 enabled 컬럼으로 관리하여 .env 하드코딩 제거 가능
  • 코드 개선: os.getenv() 직접 호출 제거, DB 조회로 대체
  • 부분적 해결: 스케줄러 관련 문제만 해결, 전체 환경변수 관리 체계 개선은 별도 작업 필요

마이그레이션 시 주의사항

  • 기존 .env 파일의 스케줄러 관련 환경변수는 DB 초기 데이터로 마이그레이션 후 제거
  • 코드에서 os.getenv() 직접 호출하는 부분을 DB 조회로 변경
  • 하위 호환성을 위해 환경변수 체크 로직은 유지하되 DB 우선 적용

주의사항

  • scheduler_status_check는 시스템 내부용이므로 DB 관리 대상 아님
  • async 함수는 래퍼 함수 필요 (_run_*_with_logging 패턴)
  • cron 표현식 파싱 로직 재사용 (_parse_cron() 함수)
  • 기존 환경변수는 하위 호환성을 위해 유지하되 DB 우선