본문 바로가기
개발[프로젝트]/쿠스토랑

식당 티어산정과 조회수 정책 변경, 음식점 목록조회 캐싱 도입

by wcwdfu 2025. 7. 31.

식당 티어산정과 조회수 정책 변경, 음식점 목록 조회 캐싱 도입

 


 

 회의 과정에서 기존 식당 티어산정 방식을 변경하기로 하였다. 

최초의 티어 책정 정책은 백분위로 나누는 것이었다. 하지만 백분위로 나누기엔 우리는 충분한 데이터를 가지고 있지 못했다. 따라서 현재는 2개 이상 평가수가 있는 모든 음식점을 대상으로 평가 점수들을 평균을 낸 뒤, 그 점수가 특정 점수 이상이면 1~5티어 내에 분포되도록 구현되어 있었다.

 

 내부적으로는 다음과 같다.

- 사용자가 음식점에 대한 평가를 진행하면 그즉시 평가완료와 함께 식당 티어까지 재 계산되는 방식이였다. 

- 코드 관점에서는 evaluation을 담당하는 매서드 내에 식당 티어 계산 로직까지 포함되어 있었다.

 

 위 방식에서 신뢰성에 한가지 문제점을 제기하였는데, 바로 평가수가 15개 지만 만점에 약간 못미치는 음식점보다 평가수가 단2개지만 만점인 음식점의 경우 티어가 전자보다 높다는 경우였다.

 따라서 기존에 restaurant 도메인을 맞아주던 딩운이 스케줄러 방식을 도입해 매일 새벽 3시마다 조회수,평가수,평점 등등 좀더 합리적인 내부로직을 기준으로 하여 티어를 재계산하도록 변경하기로 하였다. 이는 또한 평가 코드 내에 존재하던 랭킹 재산정 기능을 분리함으로써 코드가 깨끗해지는데도 기여를 하게 되었다.


 그리고 음식점 목록 조회에는 로컬 캐시를 적용하기로 했다. 음식점 상세페이지 조회가 아니라 목록조회를 요청할때마다 매번 DB를 접근하는 대신, 로컬캐시를 이용해 캐싱을 해두면 이제 단순목록조회에서 매번 DB질의 대신 로컬캐시를 이용해 더 가볍게 목록을 만들 수 있게 된다. 현제 쿠스토랑에 있는 전체 음식점은 대략 1100개 이다. 목록 렌더링시 필요한 데이터는 또한 그렇게 많지 않다 따라서 모든 음식점을 대상으로 캐싱을 해도 메모리 소비에 부담스럽지 않다고 판단된다. 스케줄링 이후에 데이터들로 24시간 캐싱을 적용해두는 방법을 선택해볼 수 있을것 같다.

 그리고, 이제부턴 조회수도 산정지표로 이용되는 만큼 신뢰성있는 지표로 자리잡아야 한다.

 

 사실 조회수는 최초부터 있었다. 하지만 당시는 사용자마다 중복으로 무한 증가가 되는 문제를 해결하기 까다롭다 생각했고, 학업해야할 부분들도 생겨 앱 출시를 하면서 잠시 화면뒷단으로 사라지게 되었는데 이번에 다시 복귀하게 되었다.

 

따라서 나는 음식점 조회에 대한 로컬캐시를 이용한 캐싱과, 회원 & 비회원별로 음식점과 게시글에 조회수 어뷰징 방지를 레디스를 이용하여 기능을 구현했다.

 

 ( 좀더 대규모 트래픽의 상황을 가정하고 대응해 본다면 다른 방식을 고려해볼 수 있다. 예를 들면 음식점 랭킹 산정 방식을 kafka등을 이용한 이벤트 기반 비동기 스트리밍 처리를 통해 실시간으로 상위랭킹을 계산해놓을 수 있다. 그렇게되면 랭킹 산정을 다시 실시간으로 바꿔볼 수도 있고, 또는 특정 시간에 제약 없이 언제든지 갱신을 진행하는등 시간의 제약이 없어지게 된다. 우리는 단일 서버를 이용하지만 만에 하나 다중서버인 경우에 랭킹을 산정하기위해 데이터가 분산되어 있는 경우에도 랭킹 산정 과정에서 특정 서버로의 부하가 쏠리는것을 방지할 수 있거나 집계에 필요한 데이터들을 한곳에 미리 모아두는것으로 예방할 수 있을것 같다. 조회수 같은 경우도 레디스에 사용자별 정보데이터의 유무에 따라 실 DB에 매번 증분 쿼리를 날리는 대신 조회수 데이터도 redis에 누적 집계한 뒤 특정 시점 또는 특정 값에 따라 실제 DB에 반영하는 방식으로 변경을 고려해볼 수도 있다.

 목록 캐싱도 음식점마다 score가 실시간으로 계산되기 때문에 redis의 zset을 이용하여 자동정렬되는 점을 이용해 볼 수도 있을것 같다.(다중 인스턴스 라면) )


1. 맛집 리스트업을 위한 주요 불변 데이터들은 모두tll 24시간으로 설정하여 로컬캐시에 저장한다. 조회는 여기서 하게된다. (잠정 연기)

처음에는 레디스를 생각했지만, 아무리생각해도 굳이 레디스를 이용해야하는 메리트가 전혀 없는것 같다. 방대한 데이터도 아니고, 서버가 재부팅되는 상황에는 반드시 다시 음식점들의 티어재계산이 이뤄지기 때문에 로컬 캐시를 이용해도 충분할 것 같다. 미약하겠지만 외부 리소스인 redis에 접근하는데 걸리는 시간도 단축할 수 있을것 같다. 갱신은 음식점 티어가 재 산정되고난 후, 이어서 처리하도록 한다.

--

까지 생각을 했지만 잠정 연기하기로 했다. 우선 우리는 ui적으로 평가하거나 즐겨찾기한 음식점에는 다음과 같은 표시를 해준다.

[ 평가함 : 초록색 체크 표시 즐겨찾기함 : 주황색 별 표시 ]

캐시한 데이터들을 기반으로 사용자의 실제 평가/ 즐겨 찾기 유무에 따른 별도 연산을 해줘야 하는데, restaurant 도메인을 내가 하지 않았다 보니 기존 구현 방식에서 기능을 추가하려면 대대적인 로직 변경이 필요할 것으로 진단되었다 ... .....

 

 위와 같이 음식점 리스트를 캐싱하는데도 캐싱해야하는것과 하지말아야하는 부분이 나뉘게된다. @cachable애노테이션을 사용할경우, 한 매서드에서 전달받은 인자들을 기준으로 캐싱을 하게 된다. 이때 위와같이 평가유무, 즐겨찾기 유무를 같이 한꺼번에 가져오기 위해 userId를 인자로 전달해주게 되는데 이렇게 되면 경우의 수는 유저의 수 만큼 경우의 수가 기하급수적으로 증가하게 된다.

 

 목록 조회의 경우, 맛집 리스트들만 캐싱을 이용하고, 이후 was에서 유저별로 평가여부, 즐겨찾기 여부를 조회해 최종 response 를 조립해주는 식으로 로직 변경이 이뤄져야 할것 같다.

 상세 페이지의 경우 고정된 값들과 유동적인 값들이 있다. 예를들어 식당명이나 메뉴 등과 같은 정보는 정적이지만 조회수, 리뷰리스트(평가,평가 댓글)등은 동적인 값들이다. 정적인 값들에 대해서만 캐싱을 적용해야 하므로 두 값들의 조회를 분리하고, 다시 was 에서 합치는 식으로 변경이 이뤄져야 할것 같다.

 

 그런데 현재의 restaurant 패키징 내의 코드는 ... 논의를 다시 해봐야겠다.

 

추가로 무지성으로 우선 캐시를 적용한 후, Jmeter를 통해 간단한 부하를 주어 테스트해보았다.

캐싱 적용 전

캐싱 적용 후

 

평균 응답속도는 6 -> 5 (ms) 로, 전체적인 tps는 약 2배정도 개선되었다. 


2. 조회수는 (회원/비회원), (웹/앱)을 구분하여 redis에 데이터를 저장해놓고, 일정 시간내에 같은 페이지 방문건에 대한 조회수 중복 증가를 방지한다. 작동 매커니즘은 대략적으로 아래와 같다.

 

 2-1. Viewer Key 생성

  • 회원 : 회원 정보를 토대로 viewerKey 생성
  • 앱 비회원 : 요청 헤더에 (커스텀)X-Device-Id 가 있으면 그 값을 토대로 viewerKey 생성
  • 웹 비회원 : 쿠키 kust_anno에 익명 ID가 있으면 그 값을 토대로 viewerKey생성
  • 위가 모두 없으면 : 새 익명 ID 발급,
    • 웹: Set-Cookie: kust_anno=<uuid> 로 내려서 브라우저에 저장
    • 앱 대비: 응답 헤더 X-Anonymous-Id: <uuid> 도 함께 내려 클라이언트가 저장 가능

 2-2. 리소스 타입(게시글/음식점)과 ID를 합쳐 Redis 키를 만든다.

 2-3. Redis에서 정해진 시간마다 중복체크를 진행한다.

 2-4. DB에 즉시 증분(+1)쿼리를 날린다.