네이버 검색 API를 사용하다 보니 사용자가 입력한 키워드가 상품 제목에 강하게 반영되는 걸 알 수 있었다.
예를 들어 사용자가 다음과 같은 키워드를 입력했다고 가정하자
[대상(남자친구), 예산(5~10만원), 상황(생일), 태그1(낚시), 태그2(장비)]
[대상(남자친구), 예산(5~10만원), 상황(생일), 태그1(게임), 태그2(헤드셋)]
여기서 예산은 가격 필터링 용도로만 사용되므로, 실제 검색에선 제외하면 다음과 같은 키워드로 검색이 이루어진다.
[남자친구, 생일, 낚시, 장비]
[남자친구, 생일, 게임, 헤드셋]
이때의 문제는 네이버 검색 API의 특성상 검색 결과가 [남자친구, 생일]
에 더 중점을 두게 되고 결과적으로 서로 다른 태그임에도 불구하고 비슷한 결과가 추천된다는 점이었다. 즉, 사용자의 상세한 요구(낚시 장비
, 게임 헤드셋
)가 제대로 반영되지 않고 상황(남자친구 생일
) 중심으로 결과가 쏠리게 되는 현상이 발생했다.
그리고 또 다른 문제점은 추천 결과에서 특정 브랜드가 지나치게 집중되는 현상이었다. 예를 들어 사용자가 전자기기와 무선 이어폰을 키워드로 선택했을 때 추천 상품 4개 모두가 삼성 갤럭시 버즈 시리즈(1, 2, 3) 등 하나의 브랜드로만 구성되는 문제가 발생했다.
이러한 문제를 해결하기 위해 내가 선택한 방법은 다음과 같다.
이 키워드들을 모두 사용해 검색하면 매우 구체적인 결과를 얻을 수 있지만 너무 좁아서 결과가 아예 없을 수도 있다. 반면 너무 일반적인 키워드로 검색하면 추천 품질이 낮아질 수 있다. 따라서 아래와 같은 우선순위를 두고 점점 더 포괄적인 키워드 조합을 생성하도록 설계했다.
가장 구체적인 것부터 → 일반적인 키워드
까지 내림차순으로 생성
각 키워드 조합은 다음 4가지 순서를 따른다.
[태그(r개) + 대상자 + 이유]
[태그(r개) + 대상자]
[태그(r개) + 이유]
[태그(r개)]
public static List<List<String>> generatePriorityCombos(List<String> tags, String receiver, String reason) {
List<List<String>> result = new ArrayList<>();
int n = tags.size();
// 가장 많은 태그 조합부터 시작
for (int r = n; r >= 2; r--) {
comboRec(tags, 0, r, new ArrayList<>(), result, receiver, reason);
}
// 태그가 하나뿐인 경우도 보장 (예외적 상황)
if (n == 1) {
comboRec(tags, 0, 1, new ArrayList<>(), result, receiver, reason);
}
// 태그가 없거나 하나 이하일 때 마지막 fallback: 대상자 + 이유만
if (tags.size() <= 1 && !receiver.isBlank() && !reason.isBlank()) {
result.add(List.of(receiver, reason));
}
return result;
}
private static void comboRec(List<String> tags, int start, int r, List<String> cur,
List<List<String>> out, String receiver, String reason) {
if (cur.size() == r) {
// 1) 태그 + 대상자 + 이유
if (!receiver.isBlank() && !reason.isBlank()) {
List<String> combo = new ArrayList<>(cur);
combo.add(receiver);
combo.add(reason);
out.add(combo);
}
// 2) 태그 + 대상자
if (!receiver.isBlank()) {
List<String> combo = new ArrayList<>(cur);
combo.add(receiver);
out.add(combo);
}
// 3) 태그 + 이유
if (!reason.isBlank()) {
List<String> combo = new ArrayList<>(cur);
combo.add(reason);
out.add(combo);
}
// 4) 태그만
out.add(new ArrayList<>(cur));
return;
}
for (int i = start; i < tags.size(); i++) {
cur.add(tags.get(i));
comboRec(tags, i + 1, r, cur, out, receiver, reason);
cur.remove(cur.size() - 1);
}
}
이 로직은 사용자의 태그를 중심으로 가장 구체적인 조합부터 생성한다.
예를 들어 태그: [낚시, 장비]
, 대상자: [남자친구]
, 상황: [생일]
이 주어졌다면 다음과 같은 순서로 검색 키워드를 생성한다.
[낚시, 장비, 남자친구, 생일]
[낚시, 장비, 남자친구]
[낚시, 장비, 생일]
[낚시, 장비]
이 순서대로 검색하기 때문에 처음부터 사용자가 원하는 구체적인 상품이 우선 추천된다. 특히 태그를 가장 중요하게 처리하여 [낚시, 장비]
와 [게임, 헤드셋]
이 확실히 구분되도록 동작한다.
추천 결과의 다양성을 높이기 위한 브랜드 중복 방지 로직을 설계하게 되었다.
브랜드 중복 방지를 구현하기 위해 처음엔 간단한 룰 기반의 브랜드 추출 로직을 만들었다. 현재로서는 가장 간단하고 빠르게 적용 가능한 방법을 사용했는데 그 이유는 우선 빠르게 문제를 해결하고 추후 데이터를 기반으로 더 정교하게 발전시키기 위함이다.
아래는 상품의 제목과 쇼핑몰 이름을 바탕으로 간단히 브랜드를 추출하는 코드이다.
public static String extractBrand(String title, String mallName) {
String lower = title.toLowerCase();
if (lower.contains("삼성")) return "삼성";
if (lower.contains("apple") || lower.contains("애플")) return "애플";
if (lower.contains("sony") || lower.contains("소니")) return "소니";
if (lower.contains("lg")) return "LG";
return mallName; // 특정 브랜드에 해당되지 않으면 쇼핑몰 이름을 브랜드로 간주
}
아래는 실제 브랜드 중복을 방지하는 코드이다.
private List<Product> pickDistinctBrandsExactly(List<Product> products, int needCount) {
Map<String, Product> brandMap = new LinkedHashMap<>();
for (Product p : products) {
String brand = RecommendationUtil.extractBrand(p.getTitle(), p.getMallName());
if (!brandMap.containsKey(brand)) {
brandMap.put(brand, p); // 최초로 나온 브랜드 상품만 담음
}
if (brandMap.size() >= needCount) break; // 원하는 개수를 채우면 종료
}
// 정확히 원하는 개수만큼 브랜드를 확보했을 때만 반환
return brandMap.size() == needCount ? new ArrayList<>(brandMap.values()) : Collections.emptyList();
}
이 로직의 실제 동작은 다음과 같다.
이러한 룰 기반 필터링 덕분에 추천의 다양성이 즉각적으로 개선되었고 사용자가 다양한 상품을 볼 수 있게 되었다.
브랜드 필터링 정교화 개선 계획
현재는 문자열 포함 여부로 브랜드를 추출하는 단순한 룰 기반 방식을 사용하고 있다. 하지만 이 방식은 신규 브랜드나 다양한 표현을 정확히 식별하지 못하고 오탐 가능성도 존재한다. 이를 개선하기 위해 브랜드 사전(Brand Dictionary)을 구축하고 키워드-브랜드 매핑 방식으로 전환할 계획이다.예를 들어 "버즈", "갤럭시버즈"는 "삼성", "에어팟"은 "애플"로 매핑하는 식이다. 상품명 내 브랜드 키워드를 기준으로 정확한 브랜드명을 추출하고 이 사전을 기반으로 추천 결과의 브랜드 중복을 더 안정적으로 제어할 수 있다. 추후 사용자 데이터와 상품 데이터를 축적하면서 이 사전을 지속적으로 확장하고, 브랜드 필터링 품질도 점진적으로 고도화할 계획이다.
이번 구현을 통해 추천 결과에서 다양성을 확보하는 것이 단순 정확도만큼이나 중요하다는 것을 다시 한 번 실감했다. 아무리 사용자의 키워드를 정확히 반영하더라도 결과가 특정 브랜드로 편향되면 추천의 신뢰도와 사용자 경험이 떨어질 수 있다.
또한 유저 테스트를 위해 빠르게 MVP를 개발할 때 복잡한 알고리즘을 적용하기 전에 단순한 룰 기반 필터링만으로도 서비스 품질을 빠르게 개선할 수 있다는 점에서 빠른 문제 해결 -> 점진적 고도화 전략의 중요성을 체감했다. 이러한 경험은 기능의 정교함보다 사용자 관점에서 당장 체감되는 문제를 우선 해결하는 접근이 훨씬 효과적이라는 것을 보여줬고 앞으로의 개선 방향에도 중요한 기준이 될 것이다.