# 아키텍처 — 어떻게 동작하는가 [← README로](../README.md) ## 1. 구성요소 (한눈에) | 구성요소 | 파일 | 역할 | |---------|------|------| | **API 서버** | `web_main.py` | 검색 요청을 받아 **큐에 적재**만 함(빠르게 응답). 상태·이력 조회 제공 | | **큐(대기줄)** | `crud/job_crud.py` + `job` 테이블 | 할 일을 순서대로 안전하게 보관 (PostgreSQL 사용) | | **워커(일꾼)** | `worker_main.py`, `worker/` | 큐에서 하나씩 꺼내 **실제 검색·판정·저장** 수행 | | **소스 어댑터** | `services/search/` | 네이버·쿠팡에서 상품 수집 (소스별 방식 캡슐화) | | **파이프라인** | `services/pipeline/` | 수집 결과를 필터·이상치 제거·최저가 정렬 | | **AI** | `services/ai/` | "같은 상품" 판정 + 검색어 생성 (OpenAI) | > **API와 워커를 분리**한 이유: 요청 접수는 즉시(가벼움), 실제 검색은 무거움(브라우저·AI). 분리하면 요청이 밀리지 않고, 워커만 따로 늘릴 수 있습니다. ## 2. 처리 파이프라인 (워커가 하는 일) 한 건의 검색 작업은 아래 단계를 거칩니다. 각 단계의 통과 건수는 `stages`로 기록됩니다(관측). ``` ① 네거티브 캐시 확인 최근 "없음"으로 확인된 상품이면 → 재검색 생략(비용 절약) ② 소스 검색 (재정제 루프, 최대 3라운드) 라운드1: 원본 검색어 → 라운드2: AI 정밀 검색어 → 라운드3: 광역 검색어 각 라운드에서 네이버 ∥ 쿠팡 동시 검색 후 병합 ③ 필터 - mall 필터 (필요 시 특정 쇼핑몰만/제외) - 가격 밴드 (요청에 현재가가 있으면 ±범위 밖 제거) ④ 이상치 제거 (IQR) 비정상적으로 싸거나 비싼 항목 제거 (오매칭·묶음 등) ⑤ AI 같은 상품 판정 후보 중 "찾는 상품과 동일한 것"만 선별 (액세서리·다른 규격 제외) → 매칭 있으면: 최저가순 정렬 → 상위 N개 반환 (found) → 매칭 0건 + 소스 정상: 다음 라운드로 → 매칭 0건 + 소스 차단: 작업 실패 처리(뒤에서 재시도) ⑥ 결과 저장 최저가 확정 + 최저가 이력 스냅샷 기록 ``` 모든 라운드에서 못 찾으면 → **not_found(정상 종료)** + 네거티브 캐시에 기록. ## 3. 재시도 로직 — "왜 실패했나"에 따라 다르게 실패는 성격이 다르므로 **두 종류로 분리**해서 처리합니다. (섞으면 무한 재시도·오작동) | 실패 종류 | 예시 | 대응 | |----------|------|------| | **기술적 실패** | 네트워크·쿠팡 차단·API 한도·AI 오류 | 잠시 후 재시도(지수 백오프), 여러 번 실패하면 **DEAD**(사람이 확인) | | **검색어 문제** | "맥심 커피"가 너무 광범위 → 0건 | **검색어를 바꿔** 재시도(정밀→광역), 다 실패하면 **not_found** | | **진짜 없는 상품** | 실제로 안 파는 상품 | 재시도 무의미 → **not_found로 정상 종료** (에러 아님) | **무한 재시도 방지**: 기술적 재시도(횟수 상한)·검색어 재시도(라운드 상한) 둘 다 유한합니다. "못 찾음"은 **실패가 아니라 정상적인 답**으로 처리해 쌓이지 않습니다. ## 4. 쿠팡 봇 차단 대응 - 쿠팡은 **Akamai 봇 차단**(JS 챌린지)이 있어, 일반 HTTP로는 못 뚫습니다 → **실제 Chrome 브라우저**(Patchright)로 통과합니다. - **프록시 IP 회전(DECODO)**: 같은 IP로 계속 두드리면 차단되므로, 일정 시간마다 다른 IP로 바꿉니다. (매 요청마다 바꾸면 오히려 의심받아 **일정 시간 유지 후 회전**) - **감지 시 즉시 IP 전환**: 차단이 감지되면 새 IP로 바꿔 재시도하고, **감지 이력**(몇 번째 요청에서 걸렸는지)을 기록해 패턴을 분석합니다. - **대역폭 절약**: 이미지·폰트 등 불필요한 리소스는 받지 않아 프록시 비용을 줄입니다. ## 5. 최저가 이력 (그래프) - **트리거 기반**: 자동 배치로 전 상품을 주기 조회하지 않고, **실제 조회된 상품만** 그 시점에 기록 → 트래픽·비용 절약. - 검색할 때마다 **네이버 최저가 / 쿠팡 최저가 / 최종 최저가**를 스냅샷으로 남깁니다. - 그래프: X축 = 조회 시각(불규칙), Y축 = 가격, 3개 선. (한쪽 소스에 그 상품이 없던 시점은 선이 비어있음 — 정상) ## 6. 설계 원칙 (참고) - **PostgreSQL을 큐로 제대로 사용**: 별도 메시지 브로커(Redis 등) 없이, 원자적 작업 할당 + 자동 복구로 유실·중복 없이 처리. - **소스 어댑터 패턴**: 네이버·쿠팡의 수집 방식 차이를 어댑터 안에 가두고, 코어는 정규화된 결과만 다룸 → 새 쇼핑몰 추가가 쉬움. - **AI로 매칭**: 상품명 형식이 제각각이라 규칙으로 고정 파싱하지 않고, AI가 "같은 상품인지" 판단. 더 깊은 내부 구현은 각 파일 상단 주석에 정리되어 있습니다.