Files
tasteby/docs/design/348-name-similarity/README.md
joungmin 49ef0322ac docs(design): #348 isNameSimilar 자모 + Sørensen-Dice (Architect)
NFD 자모 분해 + bigram 기반 Sørensen-Dice 계수로 한국어 정확도 향상.
DDG/DTO/UNIQUE는 별도 후속(외부 API/큰 리팩터링/DB 데이터 정리 필요).

설계서: docs/design/348-name-similarity/README.md (Approved)
Refs: #348 (Architect)
2026-06-15 16:09:19 +09:00

3.9 KiB

설계서: 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<String> 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+건 중복. 데이터 정리(병합/삭제) 선행 → 별도 신규 이슈