어떤 기능인가요?
현재 fetcher 파이프라인의 문제:
- 거의 모든 HTML 이 goquery 단계에서 종료 됩니다 (chain 의 lazy-load detect + browser fallback 이 일부 cover 하지만 충분치 않음)
- SPA / dynamic content 사이트의 HTML 은 "형식적으로는 받아왔으나 본문이 비어있는" 상태로 통과 — parsing 까지 가서야 빈 본문 발견
- 실패 후에도 동일 host 의 다음 fetch 가 또 goquery 로 시작 → 동일 실패 반복
본 이슈는 host 단위로 fetcher 선택 정책 을 도입하고, validation 단계의 실패 누적을 신호로 자동으로 chromedp 로 전환 + 이전 실패 raw 들을 다시 fetch 하여 정상 처리 하는 자가 학습 흐름을 구현합니다.
제안된 흐름
```
- 새 host 의 첫 fetch → fetcher rule 부재 → default goquery 로 진행
- parser_worker 가 정상 파싱 → 정상 흐름 (변경 없음)
- parser_worker 가 parse_failure / 빈 본문 감지 →
- host 단위 실패 카운터 증가 (Redis sliding window)
- 임계값 (예: 최근 1h 내 5회) 도달 시:
a. fetcher_rules 테이블에 host → chromedp UPSERT
b. 같은 host 의 실패 raw_id 들을 모아서 retry job 으로 Kafka publish
(TopicCrawl* 의 새 메타데이터 "force_fetcher=chromedp" 부착)
c. 이후 같은 host 의 새 fetch 도 fetcher_rules 조회 결과대로 chromedp 사용
- chromedp 로 retry → 정상 파싱 → contents 저장
```
무엇을 하나요?
본 이슈는 한 PR 에 다 담기엔 큼 — 단계 분리 후 별도 PR.
단계 1 — host 단위 fetcher rule 인프라 (PR 1)
DB 스키마
```sql
CREATE TABLE fetcher_rules (
id BIGSERIAL PRIMARY KEY,
host_pattern TEXT NOT NULL UNIQUE,
fetcher TEXT NOT NULL, -- 'goquery' | 'chromedp'
reason TEXT, -- 'manual' | 'auto_upgrade_validation' | ...
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_fetcher_rules_host ON fetcher_rules(host_pattern);
```
코드 변경
- `storage.FetcherRuleRepository` 인터페이스 + Postgres 구현
- `fetcher.Resolver` 신규 (rule.Resolver 와 유사 — host 단위 cache)
- ChainHandler 가 fetch 직전 `fetcher.Resolver.Resolve(host)` 호출 → 결과에 따라 chain 구성 분기
- 결과 없음 → default chain (goquery 우선 + lazy-load fallback) — 기존 동작
- 결과 "chromedp" → chromedp 단일 chain (goquery 시도 skip)
호환성
- 기존 host 는 fetcher_rules 부재 → default 동작 → 현재 동작 100% 보존
단계 2 — 실패 감지 + 카운팅 (PR 2)
실패 신호 정의
- `rule.Error{Code: ErrParseFailure}` (selector 매칭 0건)
- 본문 빈 결과 (Title / MainContent 매칭됐으나 텍스트 길이 < 임계값) — 새 신호 정의 필요
- target_type=page 만 카운팅 (list 는 맥락이 다름)
카운터 (Redis sliding window)
```
key: fetcher:fail:{host}
TTL: 1h (sliding)
```
- parser_worker 의 `handleRuleError` / 빈본문 분기에서 호스트별 카운터 INCR
- INCR 후 값 ≥ 임계값 + 현재 fetcher_rules 부재 (또는 'goquery') → 단계 3 의 upgrade 트리거
환경변수
- `FETCHER_AUTO_UPGRADE_ENABLED`: true (default true)
- `FETCHER_AUTO_UPGRADE_THRESHOLD`: 5 (default — 1h 윈도우 내 실패 횟수)
- `FETCHER_AUTO_UPGRADE_WINDOW`: 1h
단계 3 — chromedp 전환 + 실패 raw republish (PR 3)
전환 흐름
```
- 임계값 도달 감지 (단계 2)
- fetcher_rules UPSERT: host → 'chromedp', reason='auto_upgrade_validation'
- 같은 host 의 실패 raw_id 목록 수집:
- raw_contents 에서 SELECT (where host=$1 AND parse_failed_at IS NOT NULL)
- 또는 별도 `failed_raws` 테이블 / Redis SET 으로 추적
- 각 raw_id 에 대해 새 CrawlJob 생성:
- URL = raw_contents.url
- target_type = 원본과 동일
- Headers = {"force_fetcher": "chromedp", "retry_reason": "validation_upgrade", "original_raw_id": ...}
- TopicCrawl* 으로 publish — 새 fetch 사이클 진입
- ChainHandler 가 "force_fetcher" 헤더 우선 (fetcher_rules 보다 우선)
- 새 fetch 정상 처리 → 새 raw 저장 + parser 정상 처리
- 원본 raw 는 cleanup cron 이 TTL 후 정리 (이미 동작 중)
```
안전망
- 재upgrade 중복 방지: fetcher_rules 가 이미 'chromedp' 면 무시
- republish flooding 방지: 같은 host 의 republish 는 in-flight dedup (단일 트리거 사이클로 제한)
- chromedp 도 실패 시: 단계 2 의 카운터 가 chromedp 결과로 다시 INCR → 임계값 도달 → fetcher_rules 가 이미 chromedp 라 변경 없음. alert/audit log 필요 — 운영자 개입 필요한 host 식별
단계 4 — 운영 도구 (PR 4 또는 #172 와 합류)
결정 / 모호 영역 (PR 본문에서 구체화)
| 항목 |
후보 |
| 카운팅 단위 |
host 만 (본 이슈) vs (host, target_type) vs (host, path) |
| "빈 본문" 임계값 |
Title 또는 MainContent 길이 < 100 chars? 별도 결정 |
| 임계값 / 윈도우 |
5회 / 1h (default) — 운영 데이터 보고 조정 |
| 양방향 전환 |
본 이슈 = chromedp 전환만 (upgrade-only). 다시 goquery 로 downgrade 는 별도 이슈 (chromedp 비용 ↑ 라 자동 downgrade 위험) |
| fetch republish 대상 |
(a) 직전 N건 실패 raw 만 / (b) 같은 host 의 미정리 raw 전체 — (a) 권장 (안전) |
| force_fetcher header 보안 |
외부 source 가 임의 fetcher 강제 못하도록 internal-only validation |
의존 / 연관
영향 / 위험
효과
위험
- chromedp 비용 폭주: false positive 로 잘못 upgrade 된 host 가 chromedp 로 fetch 계속 → 응답 시간 / 메모리 ↑. 임계값 보수적 (default 5회) + audit log 로 가시화
- republish 의 chained job 폭증: 한 host 에 실패 raw 100건 있으면 republish 100건 동시 발생. cap (예: max 20건/upgrade 사이클) + 시간 분산 필요
- upgrade 후 chromedp 도 실패 시 무한 reprocess 위험: 이미 chromedp 면 추가 republish 안 함 (안전망). alert 로 운영자 개입 유도
- fetcher_rules 와 parsing_rules 의 lifecycle 분리: 운영자 헷갈림 가능. CLI 도구로 통합 view 제공 권장
롤백
- 환경변수 `FETCHER_AUTO_UPGRADE_ENABLED=false` → 즉시 자동 전환 비활성. 이미 chromedp 인 host 는 유지 (manual override 만 변경)
- 코드 revert: PR 별 분리 revert. 단계 1 만 남기면 운영자 manual fetcher rule 설정 가능
- DB rollback: `DROP TABLE fetcher_rules` (down migration)
참고
- 본 이슈의 background:
- 운영 debug.log 분석 결과 — chromedp 자동 fallback 59건 / browser fetch 실패 21건 / circuit breaker 99건 — fetcher 선택의 1차 결정이 host 별로 달랐어야 함을 시사
- chain 의 lazy-load detect 는 일부 cover 하지만 "정적 HTML 받았으나 본문 없음" 케이스는 못 잡음
- 관련 코드:
어떤 기능인가요?
현재 fetcher 파이프라인의 문제:
본 이슈는 host 단위로 fetcher 선택 정책 을 도입하고, validation 단계의 실패 누적을 신호로 자동으로 chromedp 로 전환 + 이전 실패 raw 들을 다시 fetch 하여 정상 처리 하는 자가 학습 흐름을 구현합니다.
제안된 흐름
```
a. fetcher_rules 테이블에 host → chromedp UPSERT
b. 같은 host 의 실패 raw_id 들을 모아서 retry job 으로 Kafka publish
(TopicCrawl* 의 새 메타데이터 "force_fetcher=chromedp" 부착)
c. 이후 같은 host 의 새 fetch 도 fetcher_rules 조회 결과대로 chromedp 사용
```
무엇을 하나요?
본 이슈는 한 PR 에 다 담기엔 큼 — 단계 분리 후 별도 PR.
단계 1 — host 단위 fetcher rule 인프라 (PR 1)
DB 스키마
```sql
CREATE TABLE fetcher_rules (
id BIGSERIAL PRIMARY KEY,
host_pattern TEXT NOT NULL UNIQUE,
fetcher TEXT NOT NULL, -- 'goquery' | 'chromedp'
reason TEXT, -- 'manual' | 'auto_upgrade_validation' | ...
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_fetcher_rules_host ON fetcher_rules(host_pattern);
```
코드 변경
호환성
단계 2 — 실패 감지 + 카운팅 (PR 2)
실패 신호 정의
카운터 (Redis sliding window)
```
key: fetcher:fail:{host}
TTL: 1h (sliding)
```
환경변수
단계 3 — chromedp 전환 + 실패 raw republish (PR 3)
전환 흐름
```
```
안전망
단계 4 — 운영 도구 (PR 4 또는 #172 와 합류)
결정 / 모호 영역 (PR 본문에서 구체화)
의존 / 연관
영향 / 위험
효과
위험
롤백
참고