선물 추천 키워드 조합 및 브랜드 중복 필터링 구현기

송현진·2025년 5월 16일
0

프로젝트 회고

목록 보기
1/22

네이버 검색 API를 사용하다 보니 사용자가 입력한 키워드가 상품 제목에 강하게 반영되는 걸 알 수 있었다.

예를 들어 사용자가 다음과 같은 키워드를 입력했다고 가정하자

  • [대상(남자친구), 예산(5~10만원), 상황(생일), 태그1(낚시), 태그2(장비)]
  • [대상(남자친구), 예산(5~10만원), 상황(생일), 태그1(게임), 태그2(헤드셋)]

여기서 예산은 가격 필터링 용도로만 사용되므로, 실제 검색에선 제외하면 다음과 같은 키워드로 검색이 이루어진다.

  • 첫 번째 케이스: [남자친구, 생일, 낚시, 장비]
  • 두 번째 케이스: [남자친구, 생일, 게임, 헤드셋]

이때의 문제는 네이버 검색 API의 특성상 검색 결과가 [남자친구, 생일]에 더 중점을 두게 되고 결과적으로 서로 다른 태그임에도 불구하고 비슷한 결과가 추천된다는 점이었다. 즉, 사용자의 상세한 요구(낚시 장비, 게임 헤드셋)가 제대로 반영되지 않고 상황(남자친구 생일) 중심으로 결과가 쏠리게 되는 현상이 발생했다.

그리고 또 다른 문제점은 추천 결과에서 특정 브랜드가 지나치게 집중되는 현상이었다. 예를 들어 사용자가 전자기기와 무선 이어폰을 키워드로 선택했을 때 추천 상품 4개 모두가 삼성 갤럭시 버즈 시리즈(1, 2, 3) 등 하나의 브랜드로만 구성되는 문제가 발생했다.

해결 방향과 로직 설계 고민

이러한 문제를 해결하기 위해 내가 선택한 방법은 다음과 같다.

1. 키워드 우선순위 조합 생성 (generatePriorityCombos)

이 키워드들을 모두 사용해 검색하면 매우 구체적인 결과를 얻을 수 있지만 너무 좁아서 결과가 아예 없을 수도 있다. 반면 너무 일반적인 키워드로 검색하면 추천 품질이 낮아질 수 있다. 따라서 아래와 같은 우선순위를 두고 점점 더 포괄적인 키워드 조합을 생성하도록 설계했다.
가장 구체적인 것부터 → 일반적인 키워드까지 내림차순으로 생성

각 키워드 조합은 다음 4가지 순서를 따른다.

  1. [태그(r개) + 대상자 + 이유]
  2. [태그(r개) + 대상자]
  3. [태그(r개) + 이유]
  4. [태그(r개)]

실제 구현한 키워드 조합 우선순위 생성 로직 (generatePriorityCombos)

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);
    }
}

이 로직은 사용자의 태그를 중심으로 가장 구체적인 조합부터 생성한다.
예를 들어 태그: [낚시, 장비], 대상자: [남자친구], 상황: [생일]이 주어졌다면 다음과 같은 순서로 검색 키워드를 생성한다.

  1. [낚시, 장비, 남자친구, 생일]
  2. [낚시, 장비, 남자친구]
  3. [낚시, 장비, 생일]
  4. [낚시, 장비]

이 순서대로 검색하기 때문에 처음부터 사용자가 원하는 구체적인 상품이 우선 추천된다. 특히 태그를 가장 중요하게 처리하여 [낚시, 장비][게임, 헤드셋]이 확실히 구분되도록 동작한다.

2. 브랜드 중복 방지 필터링

추천 결과의 다양성을 높이기 위한 브랜드 중복 방지 로직을 설계하게 되었다.

브랜드 중복 방지 필터링 로직 구현

브랜드 중복 방지를 구현하기 위해 처음엔 간단한 룰 기반의 브랜드 추출 로직을 만들었다. 현재로서는 가장 간단하고 빠르게 적용 가능한 방법을 사용했는데 그 이유는 우선 빠르게 문제를 해결하고 추후 데이터를 기반으로 더 정교하게 발전시키기 위함이다.

아래는 상품의 제목과 쇼핑몰 이름을 바탕으로 간단히 브랜드를 추출하는 코드이다.

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();
}

이 로직의 실제 동작은 다음과 같다.

  • 검색 결과에서 상품을 차례대로 확인하며 브랜드를 추출한다.
  • 추출한 브랜드가 이미 선택된 브랜드라면 건너뛰고 처음 등장한 브랜드라면 결과에 담는다.
  • 원하는 추천 개수만큼 브랜드가 모이면 즉시 결과를 반환한다.
  • 따라서 결과가 삼성 버즈 1, 2, 3 등과 같은 동일 브랜드 상품으로만 채워지지 않고 다양한 브랜드가 골고루 추천된다.

이러한 룰 기반 필터링 덕분에 추천의 다양성이 즉각적으로 개선되었고 사용자가 다양한 상품을 볼 수 있게 되었다.

브랜드 필터링 정교화 개선 계획
현재는 문자열 포함 여부로 브랜드를 추출하는 단순한 룰 기반 방식을 사용하고 있다. 하지만 이 방식은 신규 브랜드나 다양한 표현을 정확히 식별하지 못하고 오탐 가능성도 존재한다. 이를 개선하기 위해 브랜드 사전(Brand Dictionary)을 구축하고 키워드-브랜드 매핑 방식으로 전환할 계획이다.

예를 들어 "버즈", "갤럭시버즈"는 "삼성", "에어팟"은 "애플"로 매핑하는 식이다. 상품명 내 브랜드 키워드를 기준으로 정확한 브랜드명을 추출하고 이 사전을 기반으로 추천 결과의 브랜드 중복을 더 안정적으로 제어할 수 있다. 추후 사용자 데이터와 상품 데이터를 축적하면서 이 사전을 지속적으로 확장하고, 브랜드 필터링 품질도 점진적으로 고도화할 계획이다.

📝 배운 점

이번 구현을 통해 추천 결과에서 다양성을 확보하는 것이 단순 정확도만큼이나 중요하다는 것을 다시 한 번 실감했다. 아무리 사용자의 키워드를 정확히 반영하더라도 결과가 특정 브랜드로 편향되면 추천의 신뢰도와 사용자 경험이 떨어질 수 있다.

또한 유저 테스트를 위해 빠르게 MVP를 개발할 때 복잡한 알고리즘을 적용하기 전에 단순한 룰 기반 필터링만으로도 서비스 품질을 빠르게 개선할 수 있다는 점에서 빠른 문제 해결 -> 점진적 고도화 전략의 중요성을 체감했다. 이러한 경험은 기능의 정교함보다 사용자 관점에서 당장 체감되는 문제를 우선 해결하는 접근이 훨씬 효과적이라는 것을 보여줬고 앞으로의 개선 방향에도 중요한 기준이 될 것이다.

profile
개발자가 되고 싶은 취준생

0개의 댓글