HeatTrip
사용자의 감정 상태를 기반으로 여행지를 추천하고 일정·감정 일기·회고까지 잇는 감정 중심 모바일 여행 서비스. LLM 추천을 비용·장애 관점에서 단계화하고, 검색/페이지네이션/관측성까지 직접 설계·구현했습니다.
Role
팀 리드 · 백엔드 중심
Period
2025 · 한국관광공사 관광데이터 활용 공모전 (3인)
우수상
한국관광공사 데이터 활용 공모전
2025
재호출 0
동일 추천 조건 재요청 시
cat3Filter 재사용 → LLM 미호출
Offset + Cursor
페이지네이션 전략 분리
createdtime + contentid 복합 키
역할 분담
저는 팀 리드로서 감정 기반 추천 로직, LLM 추천 서버 연동, 장소 검색·페이지네이션, 운영 관측성, 인증/보안 구조를 설계하고 구현했습니다. 아래는 “기능을 붙였다”가 아니라 어떤 문제를 어떤 근거로 어떻게 풀었는지를 정리한 것입니다.
시스템 아키텍처
Flutter 클라이언트가 REST로 Spring Boot 백엔드에 접근하고, 백엔드는 MySQL·S3/CloudFront·외부 API와 통신합니다. 비용·장애 전파가 큰 LLM 추천은 Python 서버로 분리하고 Port/Adapter로 연결했습니다.
핵심 기여 1 — LLM 추천을 “단계화”해 비용·장애를 격리
LLM으로 장소를 추천하려 했지만, 가장 단순한 방법(사용자 입력 + 장소 데이터를 통째로 LLM에 전달)은 한국관광공사가 제공하는 수만 개 장소 데이터를 모두 입력 토큰으로 넣어야 해서 컨텍스트 한계와 비용 때문에 불가능하다고 판단했습니다.
LLM의 자연어 추천 능력은 살리되, 현실적인 비용으로 실현 가능한 구조를 만드는 것이 목표였습니다.
추천을 단계화했습니다. LLM은 1차로 카테고리(cat3) 만 추천하게 하고, 디테일한 랭킹은 Spring 내부 알고리즘이 담당하도록 책임을 나눴습니다.
- LLM이 추천한 category label을 Tour API의 내부
cat3코드로 변환 - 실제 장소는
cat3기반으로 필터링한 뒤, 감정 feature · 인기도 · 거리 점수를 결합해 백엔드에서 최종 랭킹 - 최초 LLM 결과의
cat3코드를 프론트가cat3Filter로 재사용 → 같은 조건의 재요청에서는 LLM을 다시 부르지 않고 랭킹만 수행
맞춤형 추천을 수행하면서도, 단계 분리로 응답 속도·장애 전파·비용 리스크를 함께 줄였습니다. 이후 Redis 캐싱(동일 입력 LLM 출력 캐싱), VectorDB 기반 RAG 전환을 다음 개선 방향으로 두고 있습니다.
핵심 기여 2 — 사용 패턴이 다른 두 페이지네이션 분리
장소 목록은 일반 페이지 조회와 모바일 무한 스크롤이라는 서로 다른 사용 패턴을 가졌습니다. Offset 방식만 쓰면 무한 스크롤에서 데이터가 많아질수록 성능 저하와 중복/누락 문제가 생길 수 있었습니다.
전략을 분리했습니다. 일반 목록은 Offset Pagination, 무한 스크롤은 createdtime + contentid를 복합 정렬 키로 쓰는 Cursor Pagination으로 구현했습니다. cursor는 Base64 토큰으로 인코딩하고, size + 1개를 조회해 다음 페이지 존재 여부를 판단했습니다.
무한 스크롤에서 안정적인 다음 페이지 조회가 가능해졌고, 동일 createdtime 데이터가 있어도 contentid 보조 정렬로 순서가 고정되어 중복/누락을 막았습니다.
핵심 기여 3 — 검색용 Read Model + FULLTEXT 분리
Tour API 원천 데이터는 장소명·카테고리·설명이 여러 테이블에 분산돼 있어, 매 검색마다 조합하면 조회 비용과 쿼리 복잡도가 커졌습니다.
장소명·카테고리명·설명 텍스트를 search_text로 반정규화하고 MySQL FULLTEXT ngram 검색을 적용했습니다. 또한 place_trait_snapshots를 두어 cat3 기반 설명/태그를 빠르게 조회하도록 했습니다.
원천 저장 구조와 검색/노출용 read model을 분리해 검색 API의 책임을 명확히 했습니다.
핵심 기여 4 — 품질·운영 관측성 기반 구축
외부 API·LLM 서버·이미지 업로드·인증 등 장애 가능성이 많았고, 추천·인증 흐름은 변경 시 회귀가 쉬운 핵심이었습니다. 개발 중엔 빠른 발견, 운영 중엔 원인 추적이 필요했습니다.
- 추천 orchestration, LLM 응답 DTO 역직렬화, 인증 서비스에 단위 테스트 작성
- JaCoCo 커버리지 리포트 + Qodana를 GitHub Actions에 연동해 정적 분석 가시화
- Controller 처리 시간·예외를 AOP로 수집, 에러는 fingerprint 기반 중복 억제 후 Slack 알림
- 민감정보 마스킹 +
correlation id로 로그 추적성과 보안 동시 고려
핵심 흐름(추천·LLM DTO·인증)의 회귀를 테스트로 방어하고, 운영 중 에러를 Slack 알림과 correlation id로 더 빠르게 인지·추적할 기반을 마련했습니다. Qodana/JaCoCo는 아직 가시화 단계지만, 핵심 패키지 중심으로 품질 게이트를 강화할 출발점이 되었습니다.
이 프로젝트를 Java → Kotlin으로 리팩토링하며 배운 내용을 블로그에 시리즈로 정리하고 있습니다.