추천 시스템 키워드 처리 리팩토링과 fallback 보완 전략

송현진·2025년 6월 13일
0

트러블슈팅

목록 보기
6/7

이전에는 AI가 추천한 키워드를 기반으로 "대상 + 키워드 + 상황" 형태의 조합을 백엔드에서 알고리즘으로 생성한 후 그 조합을 하나씩 외부 상품 API에 전달해 상품을 수집하는 구조였다. 이 방식은 조합 수가 많아질수록 API 호출 횟수도 선형적으로 증가하며 결국 외부 API의 초당/일일 호출 제한(Quota)에 쉽게 도달하게 되는 문제가 있었다. 또한 키워드 조합에 따라 의도하지 않은 상품이 추천되거나 유사한 조합에서 거의 같은 상품이 반복 추천되는 현상도 자주 발생했다.

이에 따라 이번 리팩토링에서는 조합 기반 키워드 생성 자체를 제거하고 프론트엔드에서 AI가 질문/선택지 기반으로 정제한 단일 키워드 리스트를 바로 추천 요청에 사용하도록 구조를 전환했다.

이번 리팩토링에서 변경한 주요 사항

1. 조합 기반 요청 제거 -> 단일 키워드 중심 단순화

기존에는 여러 키워드를 결합해 조합 리스트를 만들고 반복적으로 검색/저장 작업을 수행했지만 이제는 그런 로직을 완전히 제거했다. 단일 키워드 기반으로 바꾸면서 다음과 같은 장점이 생겼다.

  • 불필요한 조합으로 인한 중복 호출이 사라짐
  • 키워드마다 최대 2개의 상품만 수집하므로 효율적
  • 호출 제한(Quota) 상황에서도 빠르게 대응 가능
  • 추천 키워드가 AI에서 이미 필터링된 상태로 전달되므로 더 정제된 추천 가능
List<List<String>> combos = RecommendationUtil.generatePriorityCombos(tags, receiver, reason);
for (List<String> combo : combos) {
    naverApiClient.searchAndSave(combo);
}

이러한 개선을 통해 findTopTwoPerKeyword 메서드로 먼저 DB에서 빠르게 후보 상품을 조회한 뒤 부족한 키워드만 개별적으로 외부 API 호출로 보완하는 구조로 변경되었다.

// 1. DB 조회 (키워드마다 최대 2개씩 추천)
int expectedCount = Math.min(8, keywords.size() * 2);
List<Product> finalProducts = findTopTwoPerKeyword(keywords, minPrice, maxPrice);

// 2. 현재 상품 수가 부족하면 -> 부족한 키워드별로 외부 수집 시도
if (finalProducts.size() < expectedCount) {
    for (String keyword : keywords) {
        int dbCount = productRepository.countByKeywordAndPrice(keyword, minPrice, maxPrice);
        if (dbCount >= 2) continue;

        long count = finalProducts.stream()
                .filter(p -> p.getKeywordGroups().stream()
                        .anyMatch(g -> g.getMainKeyword().equals(keyword)))
                .count();

        if (count < 2) {
            productService.importOneOrTwoPerKeyword(keyword, minPrice, maxPrice, 2 - (int) count);
        }
    }

2. fallback 키워드 보완

추천 키워드의 상품이 충분하지 않은 경우를 대비해 "감성", "실용적인", "가성비", "인기"같은 범용적인 키워드를 추가로 사용해 부족한 수를 채우도록 설계했다. 이 fallback 키워드는 기존 키워드 리스트에 합쳐서 다시 findTopTwoPerKeyword로 조회를 시도하며, 조건이 만족되면 더 이상 호출하지 않는다. 덕분에 추천 결과의 품질은 유지하면서도 시스템이 유연하게 대응할 수 있게 되었다.

if (finalProducts.size() < expectedCount) {
    List<String> fallbackKeywords = List.of("감성", "실용적인", "가성비", "인기");
    List<String> totalKeywords = new ArrayList<>(keywords);

    for (String fallback : fallbackKeywords) {
        totalKeywords.add(fallback);
        productService.importOneOrTwoPerKeyword(fallback, minPrice, maxPrice, 2);

        finalProducts = findTopTwoPerKeyword(totalKeywords, minPrice, maxPrice);
        if (finalProducts.size() >= expectedCount) break;
    }
}

개선 이후 비교

이전 구조에서는 키워드를 조합해 외부 API를 호출하느라 코드가 복잡하고 비효율적인 반복 작업이 많았다. 호출 횟수가 많을수록 API 호출 제한에 도달하기 쉬웠고 각 조합마다 결과를 저장하는 방식이어서 DB에도 중복된 상품이 쌓일 수 있었다. 반면 이번 개선에서는 조합 생성 없이 단일 키워드 중심으로 빠르게 상품을 조회하고 키워드당 최대 2개만 수집하도록 제한함으로써 API 호출 횟수를 획기적으로 줄일 수 있었다. 또한 추천 상품 수가 부족한 상황에는 감성" 실용적인"같은 보편적인 키워드를 추가로 사용해 추천 결과를 자연스럽게 보완할 수 있게 되었고 이 덕분에 사용자는 빈약한 결과 없이 항상 일정 수준 이상의 추천을 받아볼 수 있게 되었다. 전체 흐름도 훨씬 더 유연하고 직관적인 구조로 바뀌어 유지보수나 확장에도 유리하다.

느낀 점

이번 리팩토링을 통해 추천 시스템의 키워드 처리 구조가 매우 간결하고 실용적인 형태로 개선되었다. 특히 조합 기반 호출에서 단일 키워드 기반으로 바꾸면서 호출 횟수와 코드 복잡도가 크게 줄어들었고 키워드당 최대 2개의 상품만 수집하도록 제한함으로써 API 호출 제한에도 잘 대응할 수 있게 되었다. 무엇보다 fallback 키워드 전략을 통해 상품이 부족한 상황에서도 유연하게 추천 품질을 보완할 수 있게 되어 사용자 만족도 향상에도 도움이 될 것으로 기대된다. 하지만 fallback은 사용자에 따라 다르게 설정하는 게 더 좋을 수 있다. 하지만 그 부분은 추후 생각해야되기 때문에 가장 범용적인 키워드로 설정할 생각이다.

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

0개의 댓글