프로젝트 목록으로
개인 프로젝트2025.03 - Present

PLAYLOOM (Steam Game Recommend)

Steam 게임을 취향과 조건으로 찾아주는 웹 서비스입니다. Electron 데스크톱 앱으로 시작해 웹으로 옮겨 계속 개발하고 있습니다.

내 역할

기획, 추천 엔진, 백엔드, 프론트엔드, 배포와 운영까지 전 과정을 1인 개발로 진행하고 있습니다.

결과

데스크톱 앱을 웹 서비스로 옮기고 임베딩 기반 매칭, 사용자 피드백 개인화, 추천 근거 표시를 추가했습니다.

담당 역할

기획, 추천 엔진, 백엔드, 프론트엔드, 배포와 운영까지 전 과정을 1인 개발로 진행하고 있습니다.

PLAYLOOM (Steam Game Recommend) screenshot 1

Tech Stack

Spring BootJava 17JPAPostgreSQLpgvectorFlywayNext.jsTypeScriptDockerGitHub ActionsGroqGemini APISteam Web APIResilience4j

아키텍처 / ERD

Next.js 프론트엔드가 Spring Boot API를 호출하고, 게임과 태그, 임베딩 벡터는 PostgreSQL에 저장합니다. 임베딩 검색은 pgvector를 쓰고 스키마 변경은 Flyway로 관리합니다. 요구사항 매칭은 예산, 인원, 제외 태그처럼 확실히 판정할 수 있는 조건을 조회 단계에서 먼저 거르고, 남은 후보를 임베딩 유사도로 정렬하는 순서로 처리합니다. 자연어 입력은 LLM이 조건으로 바꾸는 일만 맡고, 추천 이유는 규칙으로 만듭니다. 결과에 붙는 사유와 주의점이 모델이 지어낸 문장이 아니라 실제로 건 조건에서 나오게 하기 위해서입니다. 백엔드는 Docker 이미지로 빌드해 서버에 자동 배포하고, 게임 상세와 임베딩은 6시간 주기 배치로 갱신합니다.

주요 기능

  • 자연어 설명과 조건 입력을 함께 받는 요구사항 매칭을 구현했습니다. 예산, 인원, 최소 리뷰 수 같은 조건은 조회 단계에서 거르고 남은 후보를 임베딩 유사도로 정렬하며, 결과마다 어떤 조건에 맞아서 뽑혔는지와 무엇이 불확실한지를 함께 표시합니다.
  • 추천 결과에 좋아요와 싫어요를 남기면 다음 추천에 반영되도록 개인화를 3단계로 나눠 구현했습니다. 싫어요한 게임은 이후 추천에서 억제하고, 좋아요한 게임에서 취향 가중치를 만들며, 좋아요한 게임과 충분히 가까운 후보에만 취향 기반 설명을 붙입니다.
  • 게임을 임베딩 유사도로 연결한 유사도 그래프 탐색을 추가했습니다. 게임 하나를 고르면 가장 가까운 이웃들이 펼쳐지고, 노드를 눌러 이어서 탐색할 수 있습니다.
  • 태그 기반 랜덤, 직접 입력, Steam 프로필, 최근 플레이, 비슷한 게임 등 여러 추천 방식과 게임 선택, 리뷰 맞히기 두 가지 인터랙티브 기능을 함께 제공합니다. 초기 데스크톱 버전에서 만든 태그 동시 출현 추천과 미니게임을 웹으로 옮겨 계속 쓰고 있습니다.
  • 화면 전체를 한국어와 영어로 전환할 수 있게 만들고, 게임 상세와 임베딩을 6시간 주기로 갱신하는 배치를 붙였습니다. 초기 버전은 수집 시점 데이터가 고정되어 신규 게임이 반영되지 않는 문제가 있었습니다.

트러블슈팅

  • 한국어 입력만 계속 빈 결과가 나오던 문제문제 상황자연어 입력을 조건으로 바꾸는 단계에서 한국어 문장만 계속 아무 조건도 추출하지 못했다처음에는 사용하던 언어 모델이 한국어에 약한 것으로 판단하고 모델 교체를 검토했다해결 방안모델을 바꾸기 전에 같은 입력을 API로 직접 호출해 재현해 봤더니 모델은 세 번 모두 정상으로 답했고, 새로운 한국어 입력도 서비스에서 정상 동작했다문제가 되는 것은 특정 입력 하나뿐이었다원인은 그 입력의 첫 호출이 일시적으로 빈 결과를 냈고, 캐시가 그 빈 결과를 6시간 동안 그대로 돌려주고 있던 것이었다모든 항목이 비어 있는 파싱 결과는 캐시에 저장하지 않도록 고쳤다결과일시적인 실패가 캐시에 박혀 반복되는 경로를 없앴다진단을 건너뛰고 모델부터 교체했다면 원인을 못 찾았을 문제였다
  • 게임 이름의 특수문자가 깨지던 문제문제 상황게임 이름에 들어가는 등록상표 기호 같은 특수문자가 화면에서 깨져 보였다처음에는 위의 캐시 문제와 같은 원인으로 짐작했다해결 방안실제 원인은 응답을 만들 때 문자열을 손보던 전역 직렬화 설정이었다깨진 문자를 고치라고 넣어둔 코드가 오히려 정상적인 문자열을 망가뜨리고 있었다해당 설정을 걷어내고, 이전에 문자열을 보정하려고 넣었던 코드도 함께 정리했다결과배포 후 실제 응답에서 등록상표와 곡선 따옴표 같은 문자가 원문 그대로 나오는 것을 확인했다
  • 취향 기반 설명이 무관한 게임까지 지명하던 문제문제 상황좋아요한 게임을 근거로 추천 이유를 붙일 때, 코사인 유사도가 기준값을 넘으면 지명하도록 만들었다그런데 실제로는 임베딩 값이 좁은 범위에 몰려 있어 기준값이 변별력을 갖지 못했다아늑한 게임을 좋아한 사용자에게 전혀 다른 장르의 게임을 두고도 그 게임을 좋아해서 추천한다고 설명했다해결 방안절대적인 유사도 기준값을 버리고 순위 기준으로 바꿨다후보가 좋아요한 게임의 가장 가까운 이웃 안에 실제로 들어올 때만 취향 기반 설명을 붙이도록 했다결과설명이 붙는 조건이 임베딩 값의 분포에 좌우되지 않게 되었다
  • 외부 LLM 할당량 초과로 자연어 입력이 죽던 문제문제 상황자연어 입력을 조건으로 바꾸는 데 쓰던 외부 API가 운영 중 할당량 초과로 실패하면서, 자연어로 입력한 요청이 조건 없이 처리되었다해결 방안텍스트 처리 경로를 제공자 하나에 묶지 않고, 우선순위대로 시도하고 실패하면 다음 제공자로 넘어가는 구조로 바꿨다제공자별로 서킷 브레이커를 따로 걸어 한쪽 장애가 다른 쪽으로 번지지 않게 했다결과한 제공자가 할당량을 소진해도 자연어 입력 기능이 유지된다

성능 / 안정성

  • 임베딩 벡터를 PostgreSQL의 pgvector로 저장하고 검색해, 유사도 계산을 애플리케이션이 아니라 데이터베이스에서 처리하도록 했습니다.
  • 예산이나 인원처럼 확실히 판정할 수 있는 조건은 조회 단계에서 걸러 후보 자체를 줄인 뒤 유사도 정렬을 수행합니다.
  • 자연어 파싱 결과를 캐싱해 같은 입력의 반복 호출을 줄였습니다. 다만 모든 항목이 비어 있는 결과는 저장하지 않아 일시적 실패가 캐시에 남지 않게 했습니다.
  • 게임 상세와 임베딩 갱신은 요청 처리 경로가 아니라 6시간 주기 배치에서 수행합니다.

보안 고려 사항

  • Steam과 LLM API 키는 소스 코드에 포함하지 않고 환경 변수로 주입하며, 배포 서버에서는 컨테이너 환경 파일로 관리합니다.
  • 외부 LLM 호출이 발생하는 매칭 요청에는 분당 호출 제한을 걸어 한 사용자가 할당량을 모두 소진하지 못하게 했습니다.
  • 요청 DTO에 형식 검증을 적용하고, 검증 실패는 전역 예외 처리에서 정해진 형태의 응답으로 변환합니다.
  • 전역 예외 처리로 내부 예외와 스택 트레이스가 클라이언트에 그대로 노출되지 않도록 하고, 웹으로 옮기면서 CORS 허용 대상을 배포한 프론트엔드 주소로 제한했습니다.

아쉬웠던 점

데스크톱 버전을 마무리하면서 회고에 세 가지를 남겼습니다. 사용자별 선호도가 추천에 반영되지 않는다는 것, 데이터가 수집 시점에 고정되어 신규 게임이 반영되지 않는다는 것, 추천 로직과 폴백에 대한 테스트가 부족하다는 것이었습니다. 그 뒤로 세 가지를 차례로 처리했습니다. 좋아요와 싫어요를 받아 억제와 취향 가중치, 취향 기반 설명으로 이어지는 개인화를 넣었고, 게임 상세와 임베딩을 6시간 주기로 갱신하는 배치를 붙였으며, 테스트를 백엔드 150여 개와 프론트엔드 90여 개까지 늘렸습니다. 확장하면서 배운 것은 추천 품질보다 추천 근거를 다루는 쪽이 더 어렵다는 점입니다. 이유를 언어 모델에게 쓰게 하면 문장은 그럴듯하지만 실제로 건 조건과 어긋날 수 있어서, 이유는 규칙으로 만들고 언어 모델은 입력을 조건으로 바꾸는 일만 맡도록 경계를 그었습니다. 같은 이유로 취향 기반 설명에서 절대적인 유사도 기준값을 버리고 순위 기준으로 바꿨습니다. 사용자에게 근거를 보여주기로 한 이상, 근거가 틀리면 추천이 틀린 것보다 더 나쁘다고 판단했습니다.

프로젝트 유형

개인 프로젝트

기간

2025.03 - Present

사용 기술

14 Technologies