diff --git a/docs/design/348-name-similarity/README.md b/docs/design/348-name-similarity/README.md new file mode 100644 index 0000000..e977dc3 --- /dev/null +++ b/docs/design/348-name-similarity/README.md @@ -0,0 +1,80 @@ +# 설계서: isNameSimilar 한국어 자모 분해 + Sørensen-Dice (#348) + +> **상태**: Approved +> **작성**: [AI] Architect · **최종수정**: 2026-06-15 +> **추적성** — Redmine: #348 · 부모: #332 (이미 close) +> · 구현 파일: `backend-java/src/main/java/com/tasteby/util/HangulSimilarity.java` (신규), `backend-java/src/main/java/com/tasteby/controller/RestaurantController.java` + +## 1. 목적 + +기존 `isNameSimilar`가 Jaccard-like(문자 set 교집합 비율 ≥ 0.4)로 짧은 한국어 이름에서 오탐 가능. 자모 분해 + Sørensen-Dice bigram으로 정확도 향상. + +## 2. 범위 + +- **포함** + - `HangulSimilarity.similarity(a, b)` 유틸 신규 + - `RestaurantController.isNameSimilar` 호출부를 새 유틸로 교체 +- **제외 (별도 후속으로 분리)** + - DDG → 정식 검색 API 전환 (외부 API 결정 + 비용/계약 필요) + - DTO RestaurantUpdateDTO + @Valid 표준화 (#332 화이트리스트 set으로 SQL 측 가드 확보) + - UNIQUE(google_place_id) 제약 강화 — DB 중복 정리 선행 필요(현재 10+건 중복 확인) + +## 3. 인수조건 + +- [ ] `HangulSimilarity.similarity(a, b)` 0.0~1.0 반환 (1.0=동일) +- [ ] 한국어 음절을 Unicode NFD로 자모 분해(초성·중성·종성) +- [ ] 분해 후 bigram 기반 Sørensen-Dice 계수 계산 +- [ ] 빈 문자열 안전 처리 (둘 다 비면 0.0, 한쪽만 비면 0.0) +- [ ] `RestaurantController.isNameSimilar` 임계값 0.45로 호출 (Jaccard 0.4와 유사 보수성) +- [ ] 회귀 없음 — 기존 정상 매칭 시나리오 통과 + +## 4. 컨텍스트 & 제약 + +- Java 21 `Normalizer.normalize(Form.NFD)` 활용. +- 한글 음절(가-힣) NFD → 초성(ㄱ-ㅎ 호환자모 또는 조합자모) + 중성 + 종성. +- 영문/숫자는 그대로 통과. +- Sørensen-Dice: `2 * |A ∩ B| / (|A| + |B|)` — bigram 다중집합(multiset) 기준. + +## 5. 함수 명세 + +| 함수 | 책임 | 시그니처 | +|------|------|---------| +| `decomposeHangul(s)` | NFD 자모 분해 + 공백/구두점 제거 + 소문자화 | `static String decompose(String)` | +| `bigrams(s)` | 2글자 bigram 리스트 | `static List bigrams(String)` | +| `similarity(a, b)` | Sørensen-Dice 0.0~1.0 | `static double similarity(String, String)` | + +## 6. 흐름 + +1. 두 이름을 `decompose`로 자모 분해 + 정규화. +2. 각 분해 결과를 `bigrams`로 분해. +3. multiset 교집합 크기 카운트. +4. `2 * common / (sizeA + sizeB)`. + +## 7. 엣지케이스 + +- **둘 다 빈 문자열**: 0.0 반환. +- **bigram 1개 이하**: 두 문자열 같으면 1.0, 아니면 0.0. +- **포함 관계**: 기존 코드의 `a.contains(b) || b.contains(a)` 단축 평가 유지 (1.0 반환). +- **혼합(한영)**: NFD가 한글만 분해 → 영문은 그대로. bigram 계산은 동일하게 동작. + +## 8. 테스트 (수동) + +``` +similarity("스타벅스 강남", "스타벅스 강남점") → ≥ 0.85 +similarity("스타벅스 강남", "스타벅스 종로") → ≥ 0.55, < 0.85 +similarity("스타벅스", "맥도날드") → < 0.20 +similarity("PIZZAHUT", "피자헛") → 한글 + 영문 혼재 가드 통과 +``` + +## 9. 리스크 & 대안 + +- **선택**: NFD 분해 + bigram Sørensen-Dice. Java 표준 라이브러리만 사용. +- **대안 A**: Apache Commons Text `JaroWinklerSimilarity` — 라이브러리 추가 부담. +- **대안 B**: Hangul.js류 라이브러리 — Java 포팅 없음. +- **대안 C**: Levenshtein 거리 — 자모 분해와 결합 시 좋으나 구현 복잡. + +## 10. 미해결 질문 / 분리된 후속 + +- DDG → 정식 검색 API: Naver Search API 또는 Google Custom Search (외부 API 결정 + 비용 검토 필요) — 별도 신규 이슈 +- DTO RestaurantUpdateDTO + @Valid: #332 set 화이트리스트로 1차 가드. 본격 DTO는 큰 변경 — 별도 신규 이슈 +- UNIQUE(google_place_id) 제약: 현재 10+건 중복. 데이터 정리(병합/삭제) 선행 → 별도 신규 이슈