1. 개요 및 핵심 설계 의도
고객의 상담 텍스트(문장/단어)에서 비즈니스 키워드를 추출하는 파이프라인입니다. 단순히 무거운 NLP 모델(LLM 등)에 전적으로 의존하는 대신, 속도(Performance)와 정확도(Accuracy)를 모두 잡기 위해 3단계(Stage 1~3) 하이브리드 캐스케이드(Cascade) 구조로 설계했습니다.
**💡 핵심 파이프라인 흐름 (Cascade Masking)** 분석 서비스(SqlKeywordAnalysisService)의 통제하에 순차적으로 연쇄 반응을 일으킵니다.
- 마스킹(Masking) 기법 적용: 이전 단계에서 추출이 확정된 키워드는 원문에서
*기호로 마스킹 처리하여 다음 단계로 넘깁니다. 이를 통해 Aho-Corasick이나 형태소 분석기가 이미 찾은 단어를 중복 추출하거나 오탐하는 이슈를 원천 차단했습니다.
2. 모듈별 상세 구현 명세
Step 0. 텍스트 정규화 및 위치 추적 (Normalizer)
사용자의 오타, 띄어쓰기 오류, 무의미한 특수문자 입력에 구애받지 않고 일관된 매핑 환경을 만들기 위한 전처리 모듈입니다.
- DB 적재 로직과 100% 동일한 정규화: 가장 중요하게 고려한 포인트입니다. 실시간 분석 시 사용하는 정규식(
[^a-z0-9가-힣])은 초기 DB에 마스터 키워드(Canon)와 별칭(Alias)을 적재할 때 사용한 정규화 로직과 완벽하게 동일합니다. 기준점이 같기 때문에 매칭률이 극대화됩니다. - **위치 복원 지도 (Offset Mapping):** 띄어쓰기나 특수문자를 날려버리면 문자열의 길이가 짧아져, 추후 프론트엔드에서 키워드를 하이라이팅할 때 인덱스가 어긋나는 치명적인 문제가 발생합니다. 이를 해결하기 위해 정규화된 글자가 원본 텍스트의 몇 번째 인덱스(Index)였는지 기록하는
offset_map배열을 함께 반환하여 위치 추적성을 보장했습니다.
Stage 1. 완전 일치 매퍼 (ExactMapper)
고객이 “요금조회”, “단말기 분실” 처럼 키워드나 별칭만 단답형으로 입력했을 때 사용하는 초고속 Fast-path입니다.
- **O(1) Hash Map 구조:** 굳이 Aho-Corasick이나 형태소 분석기를 켤 필요 없이, 정규화된 전체 텍스트가 메모리에 올려둔 딕셔너리에 있는지 즉시 검사합니다.
- **다중 충돌(Collision) 보존 로직:** ”요금”이라는 단어가 들어왔을 때, 이게 ‘요금조회’인지 ‘요금납부’인지 이 단계에서는 확정할 수 없습니다. 따라서 매핑 Value를 List 구조(
defaultdict(list))로 설계하여 덮어쓰지 않고, 충돌난 ID들을 모두 보따리에 싸서 **Stage 3의 동적 심사위원(Context Scorer)**에게 최종 판단을 위임하도록 설계했습니다.
Stage 2. 아호-코라식 멀티패턴 스캐너 (AhoCorasickExtractor)
긴 상담 문장(예: “요금조회가 안되고 자동이체 신청도 하고 싶은데…”) 속에서 수백~수천 개의 비즈니스 키워드를 동시다발적으로 스캔합니다. 정규식(re)으로 하나씩 찾는 것보다 압도적으로 빠릅니다.
-
**Tie-break (겹침 제거) 룰 구현:** 문장 안에서 ‘자동이체’와 ‘이체’가 동시에 스캔되는 등 단어가 겹칠 때(Overlap)를 대비해 기획 룰을 코드로 녹여냈습니다.
- **1순위:** 먼저 등장한 단어 우선 (
norm_start오름차순) - **2순위:** 길이가 더 긴 단어 우선 (
length내림차순) -> 즉, 부분 단어는 버리고 가장 구체적인 긴 단어를 1등으로 채택합니다.
- **1순위:** 먼저 등장한 단어 우선 (
Stage 3. 문맥 검사 및 오타 교정 심판관 (ContextScorer)
1, 2단계를 거치고도 남은 찌꺼기 텍스트(*로 마스킹 되지 않은 영역)를 처리하는 최종 Fallback 단계입니다. spaCy 한국어 모델을 활용합니다.
- **CPU 최적화 (Doc 객체 단일화):** spaCy 형태소 분석은 무겁기 때문에, 요청 1건당 무조건 딱 1번만
self.nlp()를 호출하여 문법 지도(Doc)를 생성하고 이를 각 함수에서 돌려쓰도록(Reuse) 설계했습니다. - **다중 매핑 해소 (resolve_ambiguity):** Stage 1, 2에서 결정하지 못하고 올라온 중의적 단어(예: “요금”) 주변의 문맥(Context)을 분석합니다. 주변 텍스트에 각 후보 마스터 키워드의 글자가 얼마나 포함되어 있는지 1글자당 1점씩 채점하여 가장 점수가 높은 키워드를 최종 승자로 판별합니다.
**💡 패자부활전 및 오타 교정 로직의 고민 흔적 (rescue_typos)** 단순히 유사도 알고리즘을 전체 문장에 무지성으로 돌리면 성능 이슈와 오탐(False Positive)이 폭발합니다. 이를 제어하기 위해 깐깐한 허들을 두었습니다.
- **명사 찰흙놀이:** spaCy 품사 태깅을 이용해 남은 텍스트 중 명사, 고유명사, 부사, 동사(
NOUN,PROPN,VERB등)들을 덩어리로 뭉쳐냅니다. - **O(1) 재검사 (FALLBACK_EXACT):** 띄어쓰기 이슈 등으로 2단계에서 놓쳤을 뿐, 사전에는 완벽히 존재하는 단어일 수 있으므로 유사도 검사 전 Exact 매칭을 한 번 더 수행합니다.
-
**오타 교정 (FALLBACK_TYPO) - 왜 ‘1글자’만 허용했는가?:** 초고속 문자열 비교 C++ 라이브러리인
rapidfuzz (Damerau-Levenshtein)를 도입했습니다.- **거리 제한 (Distance == 1): CS/통신 도메인 특성상 거리를 2 이상 허용해 버리면 ’해지’와 ‘정지’** 처럼 의미가 완전히 다른 단어들이 유사 단어로 묶여버리는 대참사가 발생합니다. 따라서 오직 1글자 오타(오기입, 순서 도치 등)만 교정 대상으로 삼았습니다.
- **최소 길이 제한 (
len >= 3):** 2글자 이하의 짧은 단어(‘요금’, ‘이전’)는 1글자만 달라져도 아예 다른 단어가 되므로, 오타 검사 대상에서 강제로 제외하는 안전장치를 추가했습니다.
3. 종합 출력 및 감정 분석 연동
최종적으로 1, 2, 3단계에서 수집된 모든 키워드 목록은 SqlKeywordAnalysisService에서 집계됩니다.
- 각 키워드가 등장한 횟수(
count)를 합산하고, DB에서 가져온 **이탈 위험 가중치(negativeWeight)**를 DTO에 함께 조립하여 CDC 데몬(Worker)으로 전달합니다. - 키워드 추출과 독립적으로 문장 전체의 감정(Sentiment)은
KoELECTRA기반의 감정 분석기에 태워 긍정/부정 스코어를 함께 반환하도록 구성했습니다.