FLIK: 여행코스 추천 서비스 개발기 - (백엔드 4편)

코스 자동 생성 알고리즘 설계 & 벡터 기반 개인화 로직

이전 편까지는 관광지 데이터를 수집하고 정제해 Spot DB로 저장하는 과정을 다뤘다.
이제 FLIK의 핵심 기능 — “사용자 맞춤형 여행 코스 생성” — 의 설계와 구현을 소개한다.


문제 정의

FLIK의 여행 코스 생성 기능은 단순한 랜덤 추천이 아니라,
사용자의 취향 + 장소 특성 + 이동 동선을 함께 고려해 코스를 구성한다.

기본 플로우는 다음과 같다 👇

1️⃣ 사용자가 선호하는 카테고리를 선택한다.
2️⃣ 시스템이 해당 카테고리에 맞는 장소 후보를 추천한다.
3️⃣ 사용자는 스와이프를 통해 마음에 드는 장소를 저장한다.
4️⃣ 특정 조건이 충족되면 자동으로 코스가 생성된다.


💡 사용자 입력 & 데이터 흐름

1️⃣ 사용자 입력 조건

사용자는 다음 카테고리 중 2개 이상, 4개 이하를 선택한다.

🌿 자연
☕ 카페
🏛️ 문화역사
🧗 액티비티
🏰 테마파크
🏮 축제
🏠 전통시장
🏚️ 실내여행지

2️⃣ 장소 검색

서버는 사용자가 선택한 카테고리에
자동으로 식당(restaurant)과 숙박(accommodation)을 추가하고,
카테고리별로 20개씩 장소를 검색한다.

검색 결과는 라운드 로빈(round robin) 방식으로 섞어 카드 UI로 사용자에게 노출된다.

자연 → 문화 → 카페 → 전통시장 → 자연 → 문화 → …  

3️⃣ 스와이프 기반 선호 수집

사용자는 마음에 드는 장소를 스와이프 저장한다.
→ 이때 저장된 장소는 이후 사용자 선호 벡터(User Preference Vector) 계산에 활용된다.


벡터 기반 사용자화 설계

🔹 개념 요약

사용자의 “취향”을 수치화하기 위해 각 장소를 키워드 임베딩 벡터로 표현했다.

예: {"자연": [0.11, 0.27, 0.34, ...], "카페": [0.42, 0.09, 0.18, ...]}

사용자가 특정 카테고리의 장소를 여러 개 저장하면,
그 장소들의 키워드 벡터 평균값을 해당 카테고리의 사용자 벡터로 정의한다.

user_vector["자연"] = mean([벡터1, 벡터2, 벡터3, 벡터4, 벡터5])

→ 이후 새 장소가 저장될 때마다 자동 업데이트된다.

🔹 활용

코스 생성 시 사용자가 선택한 카테고리의 장소를 탐색할 때,
해당 사용자 벡터와 유사한 장소(코사인 유사도 기준) 를 우선적으로 추천한다.

즉, 단순히 “자연 카테고리” 안에서 무작위 선택이 아니라
이 사용자가 좋아할 법한 자연” 장소를 고르는 것이다.


🔄 RAG 도입 고민

RAG(Retrieval-Augmented Generation)를 바로 사용하지는 않았다.
현재는 벡터 검색 + 알고리즘 기반 코스 생성으로 충분하지만,
향후에는 다음과 같은 확장을 고려 중이다 👇

특정 테마 기반 코스 (“벚꽃 시즌 감성여행”, “비 오는 날 indoor 코스”)
→ RAG를 활용해 사용자 선호 + 외부 지식베이스에서 컨셉 코스 생성

즉, RAG의 Generate 단계는 향후 “콘셉트 기반 생성형 추천”에 도입 예정이다.


코스 생성 알고리즘 플로우

1️⃣ 초기화 단계

[DAY1] [카페][Spot][식당][Spot][식당][숙박]  
[DAY2] [카페][Spot][Spot][식당][Spot][숙박]  
[DAY3] [카페][Spot][식당][Spot][식당][빈칸]  

2️⃣ 카페 특별 처리


3️⃣ 숙박 특별 처리


4️⃣ 메인 배치 로직

  1. 남은 카테고리들을 스와이프 빈도순으로 정렬
  2. 각 날짜를 순회하며 카테고리별 장소를 순차 배치
  3. 특별 처리 카테고리

    • 액티비티(Activity)
    • 축제(Festival)
    • 테마파크(ThemePark) → 하루 종일 소요되므로 해당 날짜 슬롯 1개 비활성화

테스트 전략

✔ 코스 생성 테스트케이스

시나리오 기대결과
자연+카페+문화 선택 각 날짜의 첫 슬롯에 카페 존재
숙박 1박 이상 숙박시설이 마지막 슬롯에 자동 배치
축제 포함 해당 날짜의 일부 슬롯 비활성화
스와이프 빈도 높은 카테고리 우선 배치됨

테스트 코드 예시 (단위 테스트)

@Test
void shouldPlaceCafeInFirstSlot() {
    CourseGenerator generator = new CourseGenerator(userPreferences);
    Course course = generator.generate(3); // 3일 여행
    for (DaySchedule day : course.getDays()) {
        assertEquals("CAFE", day.getSlots().get(0).getCategory());
    }
}

서비스 코드 개요

public Course generateCourse(User user, List<String> selectedCategories) {
    Map<String, Double> preference = preferenceService.getUserVector(user);
    List<Spot> candidates = spotService.findTopByCategorySimilarity(preference, selectedCategories);
    return courseAssembler.build(candidates, user.getTripDays());
}

정리

이번 편에서는 “사용자 벡터 + 규칙 기반 코스 자동 생성 알고리즘” 을 설계했다.

🔜 5편 예고: “추천은 어떻게 진화하는가 – 벡터 기반 개인화와 LLM 결합의 가능성”