Case study · 05 · Side project

VOC를 KPI 우선순위로
환산하는 가중치 시스템

공개 리뷰를 KPI 노드에 매핑하고 플랫폼별 가중치로 우선순위 계산

Phase 3 배포 완료 단독 진행 (1인) 2026.06: 100% 공개 데이터 · NDA-free voc-kpi-tree.netlify.app

도서·오디오북 구독 3사(밀리의서재·리디셀렉트·Audible)의 공개 VOC 1,079건을 카테고리, KPI 노드, Pillar, NSM까지 하나의 트리로 연결하는 4-Layer LLM 파이프라인입니다. 핵심은 분류가 아니라 그 위에 얹은 Eval · Observability · Guardrails 운영 레이어입니다.

75.5%
Top-1 정확도
골든셋 143건 · 합격선 75%
0.090
ECE 확신 보정오차
목표 <0.1 달성
89.2%
가드 3종 통과
자동 분류율
39칸
3플랫폼 × 13노드
가중치 매트릭스

01 · PROBLEM빈도 집계만으로 드러나지 않는 플랫폼별 차이

구독 서비스에는 매주 수백~수천 건의 리뷰·CS가 쏟아집니다. 문제는 이게 보통 '불만 빈도순 리스트'로만 쌓인다는 것. 빈도만으로는 "지금 우리 핵심 KPI가 어떤 불만으로 위협받고 있는지", "그 불만을 어떻게 풀어야 KPI 성과로 이어지는지"를 알 수 없습니다.

"성우 발음이 어색하다" 리뷰 40건. 단순 집계로는 40건짜리 이슈 하나입니다.
Audible
낭독 음성이 곧 제품. 오디오 품질 노드(가중치 0.95)를 정통으로 타격, Retention, 구독 갱신(NSM)으로 직결.
우선순위
38.040건 × 0.95
밀리의서재
오디오는 부가 기능. 가중치 0.30.
우선순위
12.040건 × 0.30
같은 불만, 같은 빈도, 3.2배 다른 우선순위.

VOC 우선순위를 KPI Tree 노드와 연결해 가중치로 환산하지 않음. 그래서 구독 갱신을 직접 좌우하는 기능에 불만이 쌓이고 있어도, 건수가 적으면 개선 순위에서 뒤로 밀림. 가장 먼저 고쳐야 할 문제가 목록 아래쪽에 묻히는 구조

02 · SYSTEM4-Layer 아키텍처

[Layer 1 · 입력]
  빌드용    3사 공개 VOC 오프라인 스크래핑 (App Store / Google Play)
  사용자용  엑셀·CSV 업로드 + 라이브 샘플 붙여넣기
  → 본실행 1,079건 (밀리 386 · 리디 361 · Audible 332)
  → 자동 분류 성공 977 + 미분류 102건은 직접 카테고리·KPI 노드를 매핑해 정답지로 확보
  → 원천 스크래핑 3,300건, 대표 표본 1,000

[Layer 2 · 분류]  2계층 매핑 (카테고리에서 KPI 노드로)
  ① 카테고리  Claude Sonnet · 프롬프트 v3.4 (결정 트리 + few-shot 19개)
              13종 중 하나로 분류. 무엇 때문에 불만인가 (관측 언어)
  ② KPI 노드  고정 매핑 테이블 · LLM 아님
              카테고리가 정해지면 노드는 자동 결정. 어떤 지표를 움직이는가 (판단 언어)
              같은 카테고리는 항상 같은 노드로 감. "왜 이 노드인가" 설명 가능 (상세 03)

  JSON 7키 고정 스키마 강제 · temperature 0
  → primary_category / secondary_category / kpi_node / node_confidence
     / sentiment / confidence / key_phrase

[Layer 3 · 운영]  ★ 이 프로젝트의 핵심
  Eval           골든셋 143건 주기 채점 (Top-1 / Top-2 분리)
  Observability  Langfuse Cloud (EU): 비용·지연·확신도 trace
  Guardrails     PII 마스킹 / confidence 0.7 라우팅 / 차단 카테고리
                 / 02 편향 가드 / surface 미식별 가드

[Layer 4 · 출력]
  KPI 노드 × 3사 히트맵(드릴다운) / 3사 KPI Tree / 우선순위 백로그 XLSX

03 · KEY DESIGN왜 2계층인가: 관측 언어 vs 판단 언어

VOC를 KPI 노드에 바로 매핑하지 않고, 사이에 카테고리 계층을 한 층 더 뒀습니다. 운영팀 라우팅과 KPI Tree 안정성을 함께 확보하기 위한 선택입니다.

카테고리 (앞단)KPI 노드 (뒷단)
언어고객이 무엇 때문에 불만인가 (관측)그게 어떤 사업 지표를 움직이는가 (판단)
성격VOC 매핑 체계 수정 가능VOC와 별개로 수립 임의 수정 불가
쓰임새운영팀 라우팅 (어느 팀에 보낼까)PO 전략 우선순위 (어느 지표에 임팩트)
매핑 주체LLM (확률적)고정 매핑 테이블 (결정적)

분리로 얻은 것

  • LLM 추론 점프 축소: "1.5배속이 깨져요"에서 곧바로 Reading Completion Rate로 가기엔 도약이 너무 큼. 중간에 관측 가능한 04_오디오북_기능을 두면 "분류가 틀렸나 / 매핑이 틀렸나"를 따로 측정·디버깅 가능
  • KPI Tree 자산 보호: 실제로 v1 카테고리 정확도 60%가 나왔을 때 KPI Tree는 그대로 두고 카테고리만 수정해 정확도를 75.5%까지 끌어올림
  • 노드매핑 고정: 앞단 카테고리는 VOC마다 판단이 들어가지만, 뒷단 노드매핑은 카테고리 값에 따라 자동으로 결정되는 고정 테이블. "왜 이 노드로 갔는지" 항상 설명 가능
  • 청중 분리(목적 기준): 카테고리는 운영, 노드는 전략. Root cause 룰도 카테고리 층에서 먼저 걸러야 라우팅과 백로그가 둘 다 망가지지 않음

04 · THE DECISION측정, 분류 정확도를 위한 재설계

1골든셋 60건 검증 → Primary 정확도 60.0% (95% CI ±12.4%p)
✕프롬프트 수정을 통한 정확도 끌어올리기
2실제 선택: 오답 24건의 패턴을 분석
3구조적 발견: 카테고리가 '기능개선/기능추가/UX개선'처럼 관리자(만드는 쪽) 시선으로 나뉘어 있어, 무엇을 기능개선으로 보고 무엇을 기능추가로 볼지 판단이 모호
· "이상해요" "예전만 못해요"처럼 구체적 증상이 없는 추상 부정도 무조건 02_앱성능/버그로 매핑
· "캐시가 30일 후 사라져요"(정책 = 10)를 결제(11)로 매핑
4카테고리 & 분류 기준 재설계: 기능개선/기능추가/UX개선에서 읽기뷰어 / 오디오북기능 / 탐색·홈UX로. 사용자가 그 불만을 어디서 겪었는가(제품 표면) 기준으로 전면 수정
★KPI Tree는 손대지 않음: 카테고리 층만 교체
✓Top-1 75.5% 도달 (합격선 달성)
같이 도입한 판단 기준 2가지
① 증상 키워드 게이트: 오류·튕김·꺼짐·먹통·강제종료·로딩 실패·다운로드 실패·동기화 실패·렉·끊김 같은 명시적 증상 키워드가 하나라도 있을 때만 02로 매핑 허용. 추상 부정만 있으면 불허.
② 06/10/11 결정 트리: 헷갈리는 3형제를 순서대로 묻게 해 사고 순서를 강제.
  Q1. 이번 1회의 결제·적립·환불 처리 문제인가? → 11 결제/환불
  Q2. 구독 구조·해지·포인트 정책 문제인가? → 10 요금제/해지
  Q3. 가격 수준·환산율 비교·결제수단 강제 문제인가? → 06 가격/가성비
'surface(제품 표면) 기반'이란: '기능개선/기능추가'처럼 만드는 쪽 시선이 아니라, '읽기뷰어'·'오디오북 재생'·'탐색/홈'처럼 사용자가 실제로 겪는 표면 단위로 카테고리를 나눈 것. 같은 불만도 어디서 겪었는지가 분류 기준이 됨
"60건 검증에서 60%가 나왔을 때, 프롬프트를 손대는 대신 오답 패턴을 분석해 카테고리 체계 자체를 다시 설계했습니다. 관리자 입장에서 정의한 모호한 카테고리 대신 사용자 불만의 출처를 기준으로 새로 설계하고, few-shot 보강과 명확한 판단 기준 추가로 정확도를 끌어올렸습니다."

05 · EVAL측정 결과: 골든셋 143건

조건: 프롬프트 v3.4 · Claude Sonnet · 정답지 144건 중 예측 확보 143건 채점

지표결과95% CI합격선판정
Top-1 정확도75.5% (108/143)[67.9, 81.8]≥75%✓ 달성
Top-2 정확도88.1% (126/143)[81.8, 92.4]≥90%△ 1.9%p 미달
감정 정확도90.9% (130/143)–80%✓
KPI 노드 정확도71.3% (102/143)–70%✓
└ 카테고리 정답 한정90.7% (98/108)––★
ECE (확신 보정오차)0.090–<0.1✓ 달성
99(분류 모호) 비율2.8%–<15%✓

KPI 노드 71.3%의 의미

KPI 노드는 카테고리 하위의 더 세밀한 지표라 정확도가 구조적으로 카테고리를 넘을 수 없습니다. KPI 오답 41건 중 31건(76%)은 카테고리도 함께 틀린 케이스: 카테고리 오답이 흘러내린 것입니다. 카테고리가 맞은 경우만 보면 KPI 정확도는 90.7%.

순수 KPI 혼동(카테고리는 맞고 노드만 틀림)은 10건뿐이고, 전부 의미가 인접한 형제 노드 사이에서 발생 (Session_Reliability ↔ Sync_Offline_Stability, Functional_Quality ↔ UX_Quality). 사람이 라벨링해도 갈리는 경계

ECE 확신도 보정: "90% 확신"이 실제로 90% 맞는지 검증

confidence 구간건수평균 확신도실제 정답률차이◦ 확신도 · ● 정답률
0.7 ~ 0.83775%57%18%p
0.8 ~ 0.97284%81%4%p
0.9 ~ 1.03092%97%5%p
왜 ECE가 중요한가: confidence를 신뢰할 수 있어야 자동 분류 임계치(0.7)가 의미를 가짐. 모델이 근거 없이 확신하면 임계치 기반 가드레일이 작동하지 않음. 목표는 ECE < 0.1로 잡았고 0.090으로 측정됨

06 · WEIGHTS가중치 매트릭스: 39칸 (3 플랫폼 × 13 노드)

초기 공식 - 우선순위 = VOC 빈도 × node_weight[node] (전역 스칼라)
문제: 3사가 같은 KPI 노드에 같은 우선순위를 가짐. 플랫폼 트리가 아무 일도 하지 않음.
개선: 가중치를 (노드, 플랫폼) 함수로 승격 → VOC 빈도 × node_weight[node][platform]
KPI 노드Pillar밀리리디Audible해석
Catalog_CoverageRetention0.900.800.803사 공통 高 구독 가치의 전제
Audio_Content_QualityRetention0.300.300.95Audible 단독 낭독 = 제품
UX_Quality (읽기뷰어)Retention0.700.900.40리디 뷰어 강점
Subscription_RenewalRevenue0.800.700.803사 공통 高 NSM 직결
Sync_OfflineContent0.750.750.75v3.0 신설 명시적 가설값
Session_ReliabilityEngagement0.300.300.30위생요인 의도적 하향

(13노드 중 6개 발췌)

설계 원칙: 플랫폼 특수성은 '노드'가 아니라 '엣지 가중치'로만 주입. 카테고리와 KPI 노드 13종은 3사 공통 불변으로 고정(이게 흔들리면 3사 비교 히트맵이 성립하지 않음). 플랫폼 차이는 오직 (노드, 플랫폼) 가중치로만 한정.

해석 규정 3가지: 이 점수를 오용하지 않기 위해

  • 중요도 ≠ 업무 우선순위: 이 점수는 전략적 중요도(Impact) 신호이지 개발 착수 순서가 아님. 실제 순서는 심각도·Effort·팀 상황이 더해져 각 Pillar 오너가 결정
  • 글로벌 랭킹 금지, Pillar별 패널 분리: 부서가 다른 노드를 하나의 큐로 줄세우지 않음. UI로 이 정의를 강제
  • 플랫폼 간 절대 비교는 가중 점수로 하지 않음: 3사 비교는 원천 VOC 빈도 히트맵으로 (가중치 분포 차이로 인한 왜곡 방지)

07 · FINDING원시 빈도 1위와 가중 우선순위 1위가 달라짐

본 표본에서는 3사 모두 원시 빈도 1위가 세션 안정성(크래시·로딩)이었습니다.
밀리 77건 · 리디 53건 · Audible 30건.

이 노드는 KPI 트리에서 위생요인으로 두어 가중치를 0.30으로 설정했습니다.
설정한 가중치를 적용하면 플랫폼별 상위 항목이 달라집니다.
플랫폼원시 빈도 1위가중 우선순위 1위성격
밀리Session_Reliability (77건)
→ 가중 4위 (23.1)
Catalog_Coverage
50건 × 0.90 = 45.0 · 부정 58%
라인업 결핍 + 동기화
리디Session_Reliability (53건)
→ 가중 5위 (15.9)
Discovery_Experience
34건 × 0.60 = 20.4 · 부정 65%
경험(발견) 이슈
콘텐츠 불만은 부정 11%뿐
AudibleSession_Reliability (30건)
→ 가중 6위 (9.0)
Sync_Offline_Stability
22건 × 0.75 = 16.5 · 부정 68%
유일하게 Revenue Pillar가 큼

건수는 적지만 가중 우선순위가 올라가는 경우

건수로 줄세우면
6위
→
가중 우선순위
2위 (14.4)

Audible의 Subscription_Renewal은 18건뿐입니다.
건수로 줄세우면 6위 Session_Reliability(30건)에도 밀리나
18건 중 17건(94%)이 부정이고, 가중치가 0.80 → 가중 우선순위 2위(14.4)로 올라옵니다.

이 점수는 검토할 이슈를 정리하는 데 사용하며, 실제 착수 순서는 심각도와 개발 부담을 함께 고려합니다.

Pillar 프로파일: 유사한 도메인인데 고칠 곳은 저마다 다름

Pillar밀리리디Audible
Retention88.858.736.6
Engagement39.936.316.7
Content (Sync)32.220.216.5
Revenue13.615.529.2
Support1.20.31.2
밀리 = 콘텐츠·유지 중심 / 리디 = 경험(발견·뷰어) 중심 / Audible = 매출(구독·요금제) 리스크 중심: 같은 구독 모델인데 아픈 곳이 완전히 다릅니다.

08 · OPS운영 레이어: Eval · Observability · Guardrails

단일 호출 위에 출력의 신뢰성을 측정·관리하는 3개 레이어를 두었습니다.

Eval: 조건이 바뀌면 이전 점수는 무효

모델이나 프롬프트가 바뀌면 이전 조건에서 나온 정확도는 전부 무효 처리하고, 바뀐 조건으로 골든셋을 다시 채점한 값만 합격/미달 판정에 사용. "테스트에서 쓴 조건과 실행에서 쓴 조건이 다르면 그 테스트 승인은 무효"를 게이트 조건으로 명문화

Observability: Langfuse Cloud (EU)

분류 1건이 얼마 비용으로 · 얼마나 걸려서 · 얼마나 확신하고 돌아갔는지 계기판으로 관측합니다.

Guardrails: 본 실행 1,079건 실측

자동 분류 89.2% (963건)사람 검토 큐 10.8% (116건)
구분건수비율목표판정
자동 분류96389.2%≥50%✓
사람 검토 큐11610.8%––
#10 신뢰도
confidence < 0.7, 검토큐
42건
#11 차단
법적·IP·금전·비방 키워드, 강제 검토
6건
#12-a Surface
표면 키워드 0개, 검토
69건
#12-b 02 편향
02 비중 > 25%면 '버그 과잉 분류' 알람
본실행 23.4% 정상  골든셋 27.3% 초과
PII
전화번호·이메일 등 자동 마스킹
상시

Silent Failure: 조용히 실패하는 경우

개발 중, 프롬프트의 {review_text} 자리표시자가 치환되지 않은 채 LLM에 전달되는 경우가 발생. LLM은 정직하게 "분류 불가(99), confidence 0.3"을 반환

진단: 운영에서 조용히 반복되면 모든 리뷰가 99로 빠지면서도 개별 confidence는 낮아, 겉으로는 정상 작동처럼 보이는 상태가 됨. 그래서 방어층을 두 겹으로 설계

  • 파이프라인 층: LLM 호출 직전, 치환되지 않은 자리표시자가 남아 있으면 그 자리에서 즉시 중단 (구현: 프롬프트에 {...} 잔존 여부를 정규식 1줄로 검사, 호출 차단)
  • 모니터링 층: 99 비율 15% 초과 시 drift 알람
"알람이 울리지 않는 실패가 더 위험" 운영 시스템 설계의 핵심은 어떤 실수를 어디서 잡는가의 다층 방어입니다.

09 · LESSONS시행착오: 분류 기준을 다시 세운 세 지점

① Root cause 우선 원칙

케이스: "결제했는데 렉 걸리며 오류가 많아서 환불 가능할까요", 시스템은 11_결제/환불로 분류.
정정: 본문의 핵심은 '렉·오류'고 환불은 그로 인한 부수 요청. → 02_앱성능/버그가 맞음

정립한 룰: VOC 카테고리는 "사용자가 무엇을 요청하는가(intent)"가 아니라 "무엇 때문에 불만인가(root cause)"를 기준으로 잡는다.

  • 결제/환불 노드의 VOC 카운트가 잘못 올라가면 → 백로그 우선순위 자체가 왜곡
  • 분류 기준을 표면 현상이 아니라 원인으로 잡아야 같은 원인의 VOC가 한 노드에 모임
→ 분류 정확도는 단순 지표가 아니라 '운영팀 라우팅 정확도'와 직결됨. 프롬프트 v3.4의 분류 규칙으로 명문화("X 때문에/X해서 Y해주세요" 구조의 X가 primary) + few-shot 예시로 이 케이스 본문을 그대로 학습시킴

② 모호한 분기는 '다축'으로 명문화

UX_Quality와 Functional_Quality의 판단 기준이 모호해 라벨링 정확도 하락 → 4축 기준 도입

축UX_Quality (인간·맥락)Functional_Quality (시스템·기술)
중심축사용자 인지·감정백엔드·하드웨어 수행 가능성
평가 척도만족도·인지비용Pass / Fail
범위흐름(Flow)점(Dot)·독립 스펙
객관성주관적객관적

"가독성 안 좋아요", 인간·만족도·흐름·주관 = UX / "음성 자동연결 사라짐", 시스템·Pass/Fail·점·객관 = Functional.

인사이트: 단일 기준으로 못 가르는 영역은 2~4개 독립 축으로 쪼개어 일관성 확보

③ 자동화의 한계를 데이터로 측정

골든셋을 키워드 후보 추출로 보강하면서, 보강안이 실제로 얼마나 채택되는지를 함께 측정했습니다.

우선순위대상채택률
1번 (미사용 카테고리)32건50%
2번 (한자리 보강)32건65%
3번 (분기 케이스)3건0% ★
3번의 채택률 0%가 핵심: 분기 케이스는 키워드로 잡히지 않고 본문 의미 판단이 필요하다는 뜻. 자동화의 한계를 감이 아니라 채택률로 측정한 결과이고, 이 지점을 넘겨 100% 자동화를 밀면 false positive가 누적됨. 키워드 자동화는 표면 신호까지, 본문 의미 판단은 사람의 몫

10 · SHIP배포 방식: 빌드·서버 의존성 제거

  • Next.js에서 자체 완결형 정적 HTML 단일 파일로 교체: 빌드·서버·의존성 0. Netlify에 드래그&드롭만으로 배포. 인터넷이 끊겨도 로컬 파일 더블클릭으로 동일 동작. 데모 중 서버·네트워크 문제로 동작하지 않을 가능성을 줄이려는 선택
  • 비용 구조 분리: 본 배치 1,079건은 Claude Code CLI(구독)로 처리해 API 비용 0원. 라이브 데모만 서버리스 함수 + API로 분리. 용도별로 비용 구조를 나눈 판단
  • Non-Goal 명시: 실트래픽·파인튜닝·풀스택 SaaS는 하지 않기로 문서에 명시
구현 범위와 다음 개선 과제

구현 범위: 공개 리뷰 분류, KPI 노드 매핑, 플랫폼별 가중치 계산, 평가·검토 라우팅을 구현했습니다. 카테고리, KPI 노드, Pillar, NSM까지 연결했습니다.

측정 결과: 골든셋 143건의 Top-1 정확도는 75.5%였고, Top-2는 88.1%로 목표 90%에 미달했습니다. 첫 측정 60% 이후 프롬프트가 아니라 카테고리 체계를 교체해 다시 측정했습니다.

다음 개선 과제: 02(버그) 카테고리의 과잉 분류를 줄이는 것입니다. 골든셋 기준 예측 분포가 27.3%로 임계(25%)를 넘습니다.

실트래픽·파인튜닝·풀스택 SaaS는 Non-Goal로 명시해 범위에서 제외했습니다.

⚠️ 수치 기준을 먼저 밝힙니다: 분류 본실행은 1,079건(배포 데이터 정본), Eval은 골든셋 143건(정답지 144건 중 예측 확보분)입니다. 라이브 데모 화면·XLSX·리포트가 전부 같은 기준입니다. Top-2 88.1%는 목표 90%에 1.9%p 미달이며, 그대로 적습니다. 미달 원인과 구조적 이유는 위 05 EVAL에 그대로 적어뒀습니다.
⚠️ 02 편향 가드는 '통과'가 아니라 '경계선'입니다: 본 실행 1,079건 기준으로는 23.4%로 임계(25%) 이하지만, 골든셋 143건 기준 예측 분포는 27.3%로 초과합니다. Eval에서도 02가 다른 카테고리를 흡수하는 오답 패턴이 관측됐습니다. 02 가드 강화가 다음 사이클의 1순위 백로그입니다.
⚠️ 라이브 데모의 실시간 분류 박스는 규칙 기반 폴백입니다 (API 키 이슈: 화면에 폴백 · 규칙기반 배지 명시). 본 분류 1,079건은 Claude Sonnet 오프라인 배치의 실제 결과이고, 라이브 박스는 흐름 시연용입니다. 실호출 경로는 이미 구현·배포돼 있고 키만 넣으면 전환됩니다.