아키텍처 개요¶
시스템 구성도¶
┌──────────────────────────────────────────────────────────────────────┐
│ trend-collector (Docker container, 105번 서버) │
│ │
│ ┌────────────┐ │
│ │ APScheduler │──── 매 60초 ────┐ │
│ └────────────┘ ▼ │
│ ┌─────────────────┐ │
│ │ trend_sources │ │
│ │ DB 조회 │ │
│ │ (is_active=1 + │ │
│ │ 수집 시간 도래) │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ CollectPipeline │ │
│ └────────┬────────┘ │
│ ┌─────────────┼─────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Step 1 │ │ Step 2 │ │ Step 3 │ │
│ │ Scrape │ │ Bronze │ │ DB Save │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │
│ :18090 (health) │ │ │ │
└────────────────────┼─────────────┼──────────────┼────────────────────┘
│ │ │
┌──────▼──────┐ ┌──▼────────┐ ┌──▼────────────┐
│ 외부 사이트 │ │ RustFS │ │ MySQL │
│ m.daum.net │ │ 104:9000 │ │ 104:13306 │
│ (스크래핑) │ │ haotoday │ │ trend_collector │
└─────────────┘ └───────────┘ └───────────────┘
스케줄링 구조¶
매 60초마다 (APScheduler)
│
▼
trend_sources 테이블 조회
│ WHERE is_active = 1
│
▼
소스별 마지막 수집 시각 확인 (trend_snapshots)
│ last_collected_at + collect_interval_minutes <= NOW() ?
│
├── 시간 안 됨 → 스킵
│
└── 시간 됨 → 해당 소스만 수집 실행
│
▼
Scrape → Bronze → DB
소스별 수집 주기는 trend_sources.collect_interval_minutes 컬럼으로 관리한다.
코드 수정 없이 DB UPDATE만으로 수집 주기 변경, 수집 중단(is_active=0)이 가능하다.
레이어 구조¶
main.py ← 엔트리포인트 + 스케줄러 + Health 서버
│ 스크래퍼 레지스트리 초기화
▼
pipeline/collector.py ← DB 조회 → 수집 대상 판별 → 수집 실행
│
├── scraper/ ← 수집 + 파싱 계층
│ ├── base.py TrendScraper ABC (확장 포인트)
│ │ ScrapedResult, ScrapedKeyword DTO
│ └── daum.py DaumTrendScraper 구현
│
├── storage/ ← 저장 계층
│ ├── rustfs.py RustFS Bronze 저장 (S3 SDK)
│ ├── database.py SQLAlchemy 엔진/세션 관리
│ └── repository.py DB CRUD + 수집 대상 조회 (get_due_sources)
│
└── domain/ ← 도메인 모델
└── models.py SQLAlchemy 엔티티 (3개 테이블)
데이터 흐름 상세¶
Step 0: 수집 대상 판별 (DB 기반)¶
매 60초마다 trend_sources 테이블에서 활성 소스를 조회하고, 마지막 수집 시각 + 수집 주기를 비교하여 수집 대상을 결정한다.
- 조건:
is_active = 1ANDlast_collected_at + interval <= NOW() - 결과: 수집할 소스 목록 (0개면 스킵)
Step 1: Scrape (수집)¶
소스별 TrendScraper 구현체가 대상 사이트에 HTTP 요청을 보내고, HTML을 파싱하여 키워드를 추출한다.
- 입력: URL
- 출력:
ScrapedResult(원본 HTML + 파싱된 키워드 리스트) - 실패 시: 에러 로그 + FAILED 스냅샷 DB 기록
Step 2: Bronze 저장 (RustFS)¶
원본 HTML과 파싱된 JSON을 RustFS Bronze 레이어에 보존한다. 재파싱, 디버깅, 감사 추적 목적.
- 입력:
ScrapedResult - 출력: S3 오브젝트 (
raw.html+parsed.json) - 실패 시: warning 로그만 남기고 Step 3로 계속 진행 (Bronze는 선택적)
Step 3: DB 저장 (MySQL)¶
정규화된 트렌드 데이터를 MySQL에 적재한다.
- 입력:
ScrapedResult+ Bronze 경로 - 출력:
trend_snapshots+trend_keywords레코드 - 실패 시: 에러 로그 + FAILED 스냅샷 DB 기록
확장 전략¶
새 소스 추가 시:
1. scraper/{source}.py 생성 (TrendScraper 상속)
2. main.py의 _init_scrapers()에 등록
3. DB trend_sources에 자동 INSERT (또는 수동 INSERT로 interval/active 설정)
파이프라인은 DB에서 활성 소스를 조회하여 레지스트리에 등록된 스크래퍼를 실행하므로, DB에서 is_active=0으로 변경하면 코드 수정 없이 수집을 중단할 수 있다.
→ 상세: ADR-004. Scraper 인터페이스
인프라 공유¶
오늘도하오(Hao-Day-Backend) 인프라를 공유하되 논리적으로 분리:
| 리소스 | 공유 방식 | 분리 기준 |
|---|---|---|
| MySQL | 같은 인스턴스 | database 분리 (trend_collector vs haotoday) |
| RustFS | 같은 버킷 | prefix 분리 (data/bronze/trend/ vs data/bronze/hsk/) |