이 글은 누구를 위한 것인가
- 리뷰 시스템을 구축하거나 리뷰 어뷰징 문제를 해결하려는 커머스 엔지니어
- 평점 조작으로 인해 상품 신뢰도가 떨어지는 플랫폼을 운영하는 팀
- 리뷰 데이터를 상품 랭킹에 반영하는 알고리즘을 설계하는 개발자
들어가며
온라인 쇼핑에서 리뷰는 구매 결정의 핵심 요소다. BrightLocal의 조사에 따르면 소비자의 79%가 온라인 리뷰를 지인 추천만큼 신뢰한다. 하지만 그만큼 조작 시도도 많다. 판매자가 자체 리뷰를 작성하거나, 경쟁 상품에 악의적 저평점을 남기거나, 리뷰 작성 대가로 선물을 제공한다.
신뢰도 있는 리뷰 시스템은 단순한 별점 수집이 아니라 진성 리뷰를 보호하고 가짜 리뷰를 탐지하는 방어 체계다.
1. 리뷰 DB 스키마 설계
CREATE TABLE reviews (
id TEXT PRIMARY KEY,
product_id TEXT NOT NULL,
user_id TEXT NOT NULL,
order_item_id TEXT REFERENCES order_items(id), -- 구매 인증
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 5),
title TEXT,
body TEXT NOT NULL,
images JSONB DEFAULT '[]', -- 첨부 이미지 URL 배열
is_verified BOOLEAN DEFAULT FALSE, -- 구매 인증 여부
quality_score DECIMAL(3,2), -- 자동 품질 스코어 (0.00~1.00)
helpful_count INTEGER DEFAULT 0,
not_helpful_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending', -- pending | approved | rejected | hidden
moderated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- 리뷰 유용성 투표
CREATE TABLE review_votes (
review_id TEXT NOT NULL REFERENCES reviews(id),
user_id TEXT NOT NULL,
vote TEXT NOT NULL CHECK (vote IN ('helpful', 'not_helpful')),
created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (review_id, user_id) -- 사용자당 1표
);
-- 리뷰 신고
CREATE TABLE review_reports (
id TEXT PRIMARY KEY,
review_id TEXT NOT NULL REFERENCES reviews(id),
reporter_id TEXT NOT NULL,
reason TEXT NOT NULL, -- fake | inappropriate | spam | irrelevant
detail TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
2. 구매 인증 리뷰
가장 기본적인 신뢰도 장치는 실제 구매자만 리뷰를 작성할 수 있게 하는 것이다.
인증 조건
-- 리뷰 작성 자격 확인
SELECT oi.id as order_item_id
FROM order_items oi
JOIN orders o ON oi.order_id = o.id
WHERE o.user_id = :user_id
AND oi.product_id = :product_id
AND o.status = 'delivered'
AND oi.id NOT IN (
SELECT order_item_id FROM reviews
WHERE order_item_id IS NOT NULL
)
LIMIT 1;
- 주문 완료(
delivered) 상태인 주문 건에만 리뷰 작성 허용 - 동일 주문 건에 중복 리뷰 방지
- 반품/취소된 주문 건 제외
비구매자 리뷰 정책
비구매자 리뷰를 완전히 차단할지, 허용하되 표시할지는 비즈니스 결정이다.
| 정책 | 장점 | 단점 |
|---|---|---|
| 구매자 전용 | 신뢰도 최고 | 리뷰 수 감소 |
| 구매자 인증 표시 | 균형 | 비인증 리뷰 노출 |
| 전체 허용 | 리뷰 수 최대 | 조작 위험 높음 |
오픈마켓 형태라면 "구매자 리뷰" 탭을 별도 제공하는 것이 좋다.
3. 평점 집계: Bayesian Average
단순 산술 평균은 리뷰 수가 적을 때 왜곡이 심하다. 리뷰 1개짜리 5.0점 상품이 리뷰 1,000개짜리 4.8점 상품보다 상위에 노출되면 안 된다.
Bayesian Average (베이지안 평균):
베이지안 평균 = (C × m + Σ ratings) / (C + n)
C: 사전 리뷰 수 (전체 평균적인 리뷰 수, 예: 10)
m: 전체 상품 평균 평점 (예: 3.7)
n: 해당 상품 리뷰 수
Σ ratings: 해당 상품 평점 합계
리뷰 수가 적을수록 전체 평균으로 당겨지는 효과가 있어, 소수 리뷰로 인한 극단적 평점을 방지한다.
-- 전체 평균 평점과 평균 리뷰 수 계산 (배치로 주기적 갱신)
WITH global_stats AS (
SELECT
AVG(rating) AS global_avg,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY review_count) AS median_count
FROM (
SELECT AVG(rating) as rating, COUNT(*) as review_count
FROM reviews WHERE status = 'approved'
GROUP BY product_id
) sub
)
SELECT
product_id,
(10 * global_avg + SUM(rating)) / (10 + COUNT(*)) AS bayesian_avg
FROM reviews r, global_stats
WHERE status = 'approved'
GROUP BY product_id, global_avg;
4. 리뷰 품질 스코어링
리뷰의 신뢰도와 유용성을 자동으로 점수화해 노출 순서에 반영한다.
품질 스코어 구성 요소
| 요소 | 가중치 | 설명 |
|---|---|---|
| 구매 인증 여부 | 30% | is_verified = true |
| 리뷰 텍스트 길이 | 20% | 50자 미만은 저품질 |
| 이미지 첨부 여부 | 15% | 실물 사진 포함 |
| 유용성 투표 비율 | 25% | helpful / (helpful + not_helpful) |
| 계정 신뢰 점수 | 10% | 작성자 계정 활동 이력 |
function calculateQualityScore(review: Review, authorTrust: number): number {
let score = 0;
// 구매 인증
if (review.isVerified) score += 0.30;
// 텍스트 품질
const textLength = review.body.length;
if (textLength >= 200) score += 0.20;
else if (textLength >= 100) score += 0.12;
else if (textLength >= 50) score += 0.06;
// 이미지 첨부
if (review.images.length > 0) score += 0.15;
// 유용성 투표
const totalVotes = review.helpfulCount + review.notHelpfulCount;
if (totalVotes > 0) {
const helpfulRatio = review.helpfulCount / totalVotes;
score += helpfulRatio * 0.25;
}
// 계정 신뢰도 (0~1 정규화)
score += authorTrust * 0.10;
return Math.min(score, 1.0);
}
5. 가짜 리뷰 탐지
패턴 기반 탐지
단기 집중 리뷰 패턴:
-- 특정 상품에 단기간 리뷰가 급증하는 경우 탐지
SELECT
product_id,
DATE_TRUNC('hour', created_at) AS hour,
COUNT(*) AS review_count
FROM reviews
WHERE created_at > NOW() - INTERVAL '7 days'
AND status = 'approved'
GROUP BY product_id, hour
HAVING COUNT(*) > 10 -- 1시간에 10개 이상은 이상 신호
ORDER BY review_count DESC;
동일 IP 다중 리뷰:
SELECT product_id, registration_ip, COUNT(*) as count
FROM reviews r
JOIN users u ON r.user_id = u.id
WHERE r.created_at > NOW() - INTERVAL '30 days'
GROUP BY product_id, registration_ip
HAVING COUNT(*) > 3;
유사 텍스트 탐지:
완전히 동일한 텍스트는 DB UNIQUE 제약으로 막을 수 있지만, 약간 변형된 유사 텍스트는 텍스트 유사도 알고리즘이 필요하다.
- 짧은 텍스트: Levenshtein 거리
- 긴 텍스트: MinHash 또는 SimHash 기반 근사 중복 탐지
- 의미 유사도: 임베딩 코사인 유사도 (LLM 기반)
계정 패턴 탐지
| 의심 패턴 | 탐지 방법 |
|---|---|
| 리뷰만 작성하는 계정 | 구매 이력 없이 리뷰만 존재 |
| 특정 판매자 집중 리뷰 | 단일 판매자 상품에만 리뷰 |
| 생성 직후 활동 | 가입 후 24시간 내 다수 리뷰 |
| 모든 상품 극단 평점 | 1점 또는 5점만 작성 |
6. 리뷰 모더레이션 플로우
리뷰 작성 요청
│
▼
자동 검증
├─ 구매 인증 확인
├─ 텍스트 유해성 필터 (욕설, 개인정보)
├─ 스팸 패턴 탐지
└─ 품질 스코어 계산
│
▼
자동 승인 조건 충족?
├─ YES → status = 'approved', 즉시 노출
└─ NO → status = 'pending', 수동 검토 대기
│
▼
운영자 검토 (pending 항목)
├─ 승인 → status = 'approved'
└─ 거절 → status = 'rejected' + 사유 저장
자동 승인 조건 예시:
- 구매 인증 리뷰 + 품질 스코어 0.4 이상 + 유해 키워드 없음
맺으며
리뷰 시스템의 신뢰도는 플랫폼 전체의 신뢰도다. 구매 인증만 도입해도 기본적인 가짜 리뷰를 걸러낼 수 있다. 평점 집계에 Bayesian Average를 적용하면 소수 리뷰 조작의 효과를 줄일 수 있다.
가짜 리뷰 탐지는 규칙 기반으로 시작해 실제 어뷰징 패턴 데이터가 쌓이면 ML 모델로 발전시키는 것이 현실적이다. 처음부터 복잡한 모델이 필요하지 않다 — 단기 집중 패턴 탐지만으로도 80%의 명백한 어뷰징을 잡아낼 수 있다.