비개발자/기획자/개발자 누구나 이해하도록 일목요연하게 정리. 길이 분산 위해 분할. - README.md: 3줄 요약 + 워크플로우 다이어그램 + 주요기능 + 빠른시작 + 문서 목차 - docs/architecture.md: 구성요소·처리 파이프라인·재시도/프록시/봇감지/이력 원리 - docs/database.md: 테이블 4종(job/price_history/search_negative/bot_detection) + 코드값 - docs/api.md: 엔드포인트 요청/응답 예시(curl/Postman), 상태·코드 요약 - docs/operations.md: 실행·로그·DB조회·테스트·문제해결·배포유의 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
81 lines
5.0 KiB
Markdown
81 lines
5.0 KiB
Markdown
# 아키텍처 — 어떻게 동작하는가
|
|
|
|
[← 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가 "같은 상품인지" 판단.
|
|
|
|
더 깊은 내부 구현은 각 파일 상단 주석에 정리되어 있습니다.
|