Files
joungmin f1164b63c5 docs(design): #357 정식 검색 API 전환 설계서 (Architect)
- WebSearchService 신규: Naver Search webkr.json 우선, DDG 폴백
- searchTabling/searchCatchtable 내부 호출만 교체 (시그니처 유지)
- application.yml + k8s secrets에 NAVER_CLIENT_ID/SECRET 추가

Refs: #357

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 20:11:52 +09:00
..

설계서: DDG HTML 파싱 → 정식 검색 API 전환 (#357)

상태: Approved 작성: [AI] Architect · 최종수정: 2026-06-15 추적성 — Redmine: #357 · 부모: #348(이름 유사도, 09-Done) · 관련: searchTabling/searchCatchtable · 구현 파일: backend-java/src/main/java/com/tasteby/service/WebSearchService.java(신규), backend-java/src/main/java/com/tasteby/controller/RestaurantController.java, backend-java/src/main/resources/application.yml, k8s/secrets.yaml.template · 테스트: 본 범위 밖 (수동 — Tabling/Catchtable 매핑 SSE에서 확인)

1. 목적 (Why)

searchDuckDuckGo는 html.duckduckgo.com을 정규식으로 긁어 Tabling/Catchtable URL을 추출. 봇 차단/HTML 구조 변경 시 대량 NONE 마킹 위험. 정식 검색 API로 교체해 안정성/정확도 확보.

2. 범위 (Scope)

  • 포함
    • WebSearchService 신규 — Naver Search webkr.json 우선, 미설정/실패 시 DDG 폴백.
    • RestaurantController.searchTabling/searchCatchtable 내부 호출을 새 서비스로 교체.
    • application.yml + k8s/secrets.yaml.templateNAVER_CLIENT_ID/SECRET 항목 추가.
  • 제외 (별도 후속)
    • Kakao Local API 검색 (식당 검색 전용이라 사이트 URL 매칭 부적합).
    • Google Custom Search Engine (비용 + 무료 100/day 제약).
    • 결과 캐시/메트릭 — 후속.

3. 인수조건

  • NAVER_CLIENT_ID/SECRET 환경변수 등록 시 Naver Search webkr.json 호출.
  • 미설정 또는 5xx/timeout 시 DDG로 자동 폴백 — 회귀 없음.
  • 응답 형식: 기존 List<Map<String, Object>> 유지 — {title, url} 키.
  • URL 패턴 필터 동일 동작 (예: tabling.co.kr/restaurant/, tabling.co.kr/place/).
  • 빌드/배포 회귀 없음.

4. 컨텍스트 & 제약

  • Naver Search 무료 한도: 일 25,000건. Tabling/Catchtable 매핑 백필이 1,200 식당 × 2회 ≈ 2,400건 1회성 → 한도 내.
  • Naver는 site: 연산자 미지원 — 결과 URL 패턴 필터링으로 대체.
  • WebClient 또는 HttpClient — 기존 HttpClient(static) 재사용.
  • 키 미설정 환경(dev local 일부)에서도 DDG 폴백으로 동작 보장.

5. 아키텍처 개요

RestaurantController.searchTabling(name)
   │
   ▼
WebSearchService.search(query, urlPatterns)
   │
   ├─ NAVER_CLIENT_ID 설정 시
   │     └─ Naver webkr.json → URL 패턴 필터 → 결과
   │             (실패/0건 시 ▼)
   └─ DDG html.duckduckgo.com → 정규식 파싱 → URL 패턴 필터

6. 데이터 모델

응답 (기존 유지)

[ { "title": "스타벅스 강남대로점", "url": "https://app.catchtable.co.kr/dining/12345" } ]

Naver 응답 → 변환

{ "items": [ { "title": "<b>스타벅스</b> 강남점", "link": "https://..." } ] }
  • title<b> 태그 제거 후 사용.
  • link가 urlPatterns 중 하나에 매칭되면 결과에 포함.

7. 함수 명세

함수 책임 비고
WebSearchService.search(query, urlPatterns) 외부 진입점 Naver 우선 → DDG 폴백
WebSearchService.searchNaver(...) Naver Search webkr.json 호출 + 필터 키 미설정 시 즉시 빈 결과
WebSearchService.searchDdg(...) 기존 DDG 로직 이관 RestaurantController에서 옮김
WebSearchService.stripTags(s) <b>...</b> 제거
WebSearchService.matchesPattern(url, patterns) URL 패턴 매칭 urlPatterns.length == 0 면 모두 통과

8. 흐름

정상

  1. searchTabling(name)webSearchService.search("스타벅스", ["tabling.co.kr/restaurant/", "tabling.co.kr/place/"]).
  2. Naver 호출 → 200, items 30 → URL 패턴 매칭 0~N건 추출.
  3. 0건이면 DDG 폴백, 그래도 0건이면 빈 리스트.

폴백

  • Naver 5xx / IOException / 키 미설정 → DDG.

키 등록 (운영자 작업)

  1. https://developers.naver.com/apps/#/list 검색 앱 등록.
  2. NAVER_CLIENT_ID, NAVER_CLIENT_SECRET 발급.
  3. dev: .env에 추가, prod: k8s/secrets.yaml에 추가 + kubectl apply.

9. 엣지케이스

  • 키 미설정: searchNaver 즉시 빈 리스트 → DDG 폴백 (회귀 없음).
  • Naver rate limit (429): DDG 폴백.
  • DDG도 막힘: 빈 리스트 반환 → 호출자(매핑 SSE)에서 NONE 처리 — 기존 동작 동일.
  • URL 패턴 빈 배열: 패턴 매칭 스킵, 모든 결과 반환 (API 일반화 대비).

10. 테스트 (수동)

  • Dev: NAVER_CLIENT_ID/SECRET 등록 → 어드민 bulkTabling SSE 실행 → 매칭율 비교 (이전 DDG 대비 ≥).
  • 키 일부 제거 → DDG 폴백 동작 확인.

11. 리스크 & 대안

  • 선택: Naver primary + DDG fallback. 한국 식당 정확도 + 무료 한도 + 폴백 안정성.
  • 대안 A: Kakao Local Search — 식당 정보 직접 검색은 가능하지만 Tabling/Catchtable URL 매핑 부적합.
  • 대안 B: Google CSE — 비용/한도 제약.
  • 트레이드오프: Naver 응답 정확도가 압도적이지만 키 발급 운영자 작업 필요. 폴백으로 회귀 0 보장.

12. 미해결 질문

  • Naver의 인덱스 신선도 vs Tabling/Catchtable 최신 입점 식당 미반영 가능성 — 백필 주기 후속.
  • 결과 캐시(같은 식당 재호출) — 후속.