운영/관리자 화면에서 상품 우선순위와 노출명 가독성을 동시에 개선하고 싶었다.
이걸 위해 두 가지를 만들었다.
calculateScore(rating, reviewCount)
generateDisplayName(originalName)
먼저 설명 가능한 단순한 규칙으로 시작했다. 규칙이 많아질수록 가중치 조절과 해석이 어려워지므로 초기에는 “스위치형 누적 가점”을 채택했다. 각 임계값을 충족하면 +1을 더하는 방식이다. 이렇게 하면 왜 이 상품이 이 점수가 되었는지를 운영자가 직관적으로 이해할 수 있다.
rating ≥ 4.2
: “무난히 좋다”의 하한선. 4.0은 범위가 너무 넓고 4.5는 과도하게 타이트하다고 판단.reviewCount ≥ 100
: 초기 잡음(극단적 소수 리뷰, 지인 리뷰 등)을 어느 정도 필터링.reviewCount ≥ 1000 && rating ≥ 4.5
: 양적 신뢰(물량)와 질적 신뢰(고평점)의 교집합을 만족할 때 추가 가점.reviewCount ≥ 10000
: 대중적으로 충분히 검증된 상품에 대한 보너스.구현에서는 null‑safe를 보장하고 평점 비교는 부동소수점 오차를 피하려고 BigDecimal
과 compareTo
를 사용했다. 각 조건은 서로 독립된 if 블록으로 구성해서 이후에 규칙을 추가/수정하기 쉽도록 했다.
private int calculateScore(BigDecimal rating, Integer reviewCount) {
int score = 0;
if (rating != null && rating.compareTo(BigDecimal.valueOf(4.2)) >= 0) {
score += 1;
}
if (reviewCount != null && reviewCount >= 100) {
score += 1;
}
if (reviewCount != null && reviewCount >= 1000 && rating != null && rating.compareTo(BigDecimal.valueOf(4.5)) >= 0) {
score += 1;
}
if (reviewCount != null && reviewCount >= 10000 && rating != null && rating.compareTo(BigDecimal.valueOf(4.3)) >= 0) {
score += 1;
}
return score;
}
점수 규칙은 “임계값을 하나 넘을 때마다 +1”이 누적되는 구조라 경계값에서의 거동이 중요하다. 아래 케이스로 스스로 납득 가능성을 점검했다.
평점 4.20, 리뷰 100
rating≥4.2
(+1) + review≥100
(+1) ⇒ 2점
가장 기본적인 “무난한 품질 + 최소 표본” 조합.
평점 4.60, 리뷰 1,200
rating≥4.2
(+1) + review≥100
(+1) + review≥1000 && rating≥4.5
(+1) ⇒ 3점
양과 질의 교집합을 충족했을 때의 기대점.
평점 4.10, 리뷰 12,000
review≥100
(+1)만 충족, rating≥4.2
불충족, review≥10000 && rating≥4.3
불충족 ⇒ 1점
대중성이 매우 높아도 평점이 낮으면 상위 노출은 억제하되, 최소한의 리뷰량 기준(100↑)은 인정.
평점 4.35, 리뷰 10,500
rating≥4.2
(+1) + review≥100
(+1) + review≥10000 && rating≥4.3
(+1) ⇒ 3점
“매우 많이 팔렸고 평도 나쁘지 않은” 케이스를 확실히 끌어올린다.
평점 4.10, 리뷰 50
어떤 임계도 충족 못함 ⇒ 0점
초기 소수 리뷰의 과도한 가산점을 의도적으로 막는다.
이렇게 경계값(4.2
/4.3
/4.5
& 100
/1000
/10000
)을 따라가며 “왜 이 점수인지”를 운영자가 한눈에 설명할 수 있도록 설계했다. 특히 review≥10000
에 평점 하한(4.3
)을 둔 이유는 판매량이 많아도 평이 갈리는 상품을 과도하게 올리지 않기 위해서다.
목표는 핵심 제품명만 남기기다. 판촉 문구와 장식 기호가 섞인 타이틀은 가독성과 검색 품질(유사도, 브랜드 추출, 중복 제거)에 모두 악영향을 준다. 그래서 보수적 제거(화이트리스트식)로 시작했다가 데이터 보며 점진 확장하는 전략을 택했다.
[행사] (2024) {정품보장}
같은 부가정보는 우선 제거★♥●◆◎※
등 눈에 거슬리는 장식은 삭제무료배송, 1+1, 당일발송, 추천, BEST/HOT, 세트
등은 의미 없는 소음으로 간주private String generateDisplayName(String originalName) {
if (originalName == null) return null;
String name = originalName;
// 대괄호, 소괄호, 중괄호 안 내용 제거
name = name.replaceAll("\\[.*?\\]", "")
.replaceAll("\\(.*?\\)", "")
.replaceAll("\\{.*?\\}", "");
// 특수문자/장식 기호 제거
name = name.replaceAll("[★♥●◆◎※]", "");
// 불필요한 키워드 제거
String[] removeKeywords = {
"무료배송", "빠른배송", "사은품", "당일발송",
"세트", "세트상품", "1\\+1", "2\\+1", "3\\+1",
"인기", "추천", "HOT", "Best", "BEST", "신상품"
};
for (String keyword : removeKeywords) {
name = name.replaceAll("(?i)" + keyword, ""); // 대소문자 무시
}
// 앞뒤 공백 및 중복 공백 제거
name = name.trim().replaceAll("\\s{2,}", " ");
return name;
}
원본: [행사] (2024) ★BEST★ 러닝화 1+1 무료배송
→ 결과: 러닝화
원본: ♥인기♥ 북유럽 감성 머그컵 (세트상품) {사은품}
→ 결과: 북유럽 감성 머그컵
원본: HOT Best 신상품 프리미엄 토트백 2+1
→ 결과: 프리미엄 토트백
엣지 케이스: ‘세트’가 실제 모델명/제품군의 필수 의미일 수 있어 오탐 위험이 있다. 카테고리별 예외 키워드(whitelist)를 별도로 두고 장식/판촉 제거는 보수적으로 유지할 계획이다. 전부 지워져 빈 문자열이 될 가능성도 있으므로 후처리에서 원제목 일부를 fallback으로 남기는 방안도 고려 중이다.
이 로직은 실제 데이터를 돌려서 나온 게 아니라 내가 생각한 서비스 목표와 운영 편의성을 기준으로 만든 규칙이다. 후기·평점을 기반으로 한 가점 시스템은 복잡하게 시작하면 나중에 이유를 설명하기 어렵고 수정 시에도 영향 범위가 넓어진다. 그래서 처음부터 단순한 조건과 누적 방식으로 설계했고 각 조건을 독립적으로 만들어 이후에 쉽게 추가·삭제할 수 있게 했다.
displayName
정제도 마찬가지다. 처음부터 모든 잡음을 제거하려고 하면 오탐이 많아지고 필요한 정보까지 잃게 된다. 그래서 일단 명확하게 제거해도 되는 것부터 시작하고 필요하면 카테고리별 예외와 추가 규칙을 붙이는 식으로 발전시키는 게 맞다고 생각했다. 이 작업을 하면서 점수나 필터 규칙은 결국 “운영자가 납득할 수 있어야 한다”는 걸 다시 느꼈다. 알고리즘적으로 멋져 보여도 이유를 설명 못하면 현장에서 쓰기 힘들다. 이번 설계는 데이터 분석 없이도 내가 서비스의 방향성과 운영 시나리오를 고려해 “설명 가능성”과 “단순 조정 가능성”을 우선한 결과물이다.