추천/비추천에 대한 고민

appti·2024년 3월 28일
0

고민

목록 보기
3/3

서론

진행하고 있는 탭 관련 토이 프로젝트에서는 조회수 뿐만 아니라 추천/비추천을 통해서도 랭킹을 처리합니다.

추천/비추천의 경우 조회수만큼은 아니지만 자주 변경되고, 사용자가 눈으로 봤을 때 정상적으로 동작하지 않는다면 이질감을 느낄 수 있기 때문에 최소한의 정확성이 요구됩니다.

이를 제외하면 조회수와 유사한 부분이 많기 때문에 조회수에서 했던 고민과 유사한 방법으로 진행하도록 하겠습니다.

특징

토이 프로젝트에서 추천/비추천의 특징은 다음과 같습니다.

  • 추천/비추천은 상호 배제 방식으로 동작합니다.
  • 실시간으로 정확하게 일치할 필요는 없습니다.
  • 사용자는 추천/비추천을 취소할 수 있습니다.
  • 추천/비추천을 집계한 결과로 랭킹을 적용해야 합니다.

다음과 같이 동작합니다.

  • 사용자가 추천/비추천을 하지 않고 추천/비추천이 각각 80/12인 경우
    • 사용자가 추천을 하면 각각 81/12
    • 사용자가 비추천을 하면 80/13
  • 사용자가 추천을 해서 추천/비추천이 각각 80/12인 경우
    • 사용자가 비추천을 하면 79/13
  • 사용자가 비추천을 해서 추천/비추천이 각각 80/12인 경우
    • 사용자가 추천을 하면 81/11

서비스의 기획에 맞춰 추천/비추천 이벤트가 얼마나 발생할지 다음과 같이 예측할 수 있습니다.

  • 해당 서비스는 개인이 학습하면서 참고했던 탭을 태그별로 정리하는 기능을 제공합니다.
    장난으로 추천/비추천을 하는 경우는 있지만, 악의적으로 추천/비추천으로 공격하는 행위는 배제하고 진행하겠습니다.
    • 하나의 탭 페이지에서 단일 사용자로부터 여러 번의 추천/비추천 이벤트가 발생할 수 있습니다.
    • 일반적인 탭 페이지는 추천/비추천 이벤트가 드물게 발생합니다.
    • 최근에 랭킹에 등록된 탭 페이지는 짧은 시간동안 여러 번의 추천/비추천 이벤트가 발생할 수 있습니다.
    • 등록된지 오래된 랭킹 탭 페이지의 경우 추천/비추천 이벤트가 드물게 발생할 수 있습니다.

문제

일반적으로 추천/비추천에서 문제가 발생하는 부분은 업데이트입니다.

특히 지금 토이 프로젝트에서 추천/비추천은 상호 배제 방식으로 동작하고 있기 때문에, 업데이트 시 제대로 처리를 하지 않으면 사용자가 이질적인 느낌을 받을 수 있습니다.

실시간적으로 정확하지 않아도 되는 이유 또한, 애플리케이션 서비스 측면에서 집계를 위해 정확한 추천/비추천이 요구되는 것이라면 몰라도 사용자 입장에서는 추천/비추천 수가 정확할 필요가 없습니다.

또한 상호 배제라는 점도 충분히 성능에 악영향을 끼칠 수 있다고 생각했습니다.

사용자가 해당 탭 페이지에 추천/비추천을 했는지 체크하고, 추천/비추천을 했을 때 단순 취소가 아니라 반대로 수행하게 된다면 기존 추천/비추천에서 1을 빼고, 비추천/추천을 1 증가시키야 하기 때문입니다.

이러한 과정에서 데이터 정학성을 맞추기 위해 DB 락을 사용하게 된다면 성능에 매우 큰 악영향을 끼치게 될 것입니다.

개선 아이디어

추천/비추천 특징 중 다음과 같은 내용이 있었습니다.

  • 실시간으로 정확하게 일치할 필요는 없습니다.
  • 추천/비추천을 집계한 결과로 랭킹을 적용해야 합니다.

이는 이전에 봤었던 조회수 개선 아이디어와 유사한 상황입니다.

추천/비추천을 조회하는 기능과 추천/비추천을 집계하는 기능의 불일치로 인해 성능상의 문제가 발생할 수 있다는 것입니다.

하지만 탭 페이지를 조회하기만 하면 무조건 1이 증가하는 조회수와는 달리 추천/비추천은 사용자가 직접 제어할 수 있는, 상호 배제적인 기능이라는 차이점을 가지고 있습니다.

그러므로 다음과 같이 분리해보겠습니다.

  • 데이터가 정확해야 하는 경우
    • 사용자가 추천/비추천을 하는 경우
    • 랭킹을 집계해야 하는 경우
  • 데이터 정확하지 않아도 되는 경우
    • 사용자가 탭 페이지를 조회하는 경우

여기서 데이터가 정확해야 하는 경우도 세부적으로는 서로 다릅니다.

랭킹을 집계해야 하는 경우는 탭 페이지가 생성된 이후, 추천/비추천한 수가 정확해야 합니다.
하지만 사용자가 추천/비추천을 하는 경우, 탭 페이지 조회 시점의 추천/비추천 수가 정확할 필요가 없습니다.
사용자가 트리거한 추천/비추천 상호 배제 이벤트만을 정확하게 처리하면 됩니다.

이러한 부분을 주의하며 진행하면 될 것 같습니다.

구현 방식 아이디어

이전에 살펴본 내용을 토대로, 여러 구현 방식을 고려해보고 그 중 하나를 선택하고자 합니다.

토이 프로젝트는 단일 서버에 규모가 매우 작겠지만, 학습을 위해 최대한 다양한 아이디어를 고려해보고자 합니다.


추천/비추천은 다음과 같이 동작합니다.

  • 사용자가 추천/비추천을 하지 않은 경우
    • 추천/비추천 +1
  • 사용자가 추천/비추천을 한 경우
    • 취소 시 : 추천/비추천 -1
    • 비추천/추천 시 : 기존 추천/비추천에 -1, 새로운 비추천/추천 +1

이 중 가장 비용이 큰 연산(= 문제가 발생할 가능성이 높은 연산)은 마지막 경우이기 때문에 이 연산을 기준으로 아이디어를 진행하도록 하겠습니다.

1. 추천/비추천 시 마다 DB 업데이트

가장 간단한 구현 방식입니다.

데이터의 정합성을 맞추기 위해 락을 필수적으로 적용해야 합니다.

조회를 하고, 기존 추천/비추천 값을 1 감소시키고, 새롭게 비추천/추천 값을 1 증가시켜야 하는 긴 연산 과정을 수행할 때 까지 락을 유지해야합니다.

그러므로 비용도 비용이지만 트래픽이 몰릴 경우 락과 관련된 문제(데드 락 등등)이 발생할 가능성이 큽니다.

하지만 지금, 저 혼자서만 사용할 가능성이 매우 높은 토이 프로젝트이 방법이 가장 적합하다고 볼 수 있습니다.

2. In-Memory에 추천/비추천을 카운팅한 뒤 주기적으로 DB 업데이트

애플리케이션에서 추천/비추천을 카운팅하고, 주기적으로 DB에 업데이트 하는 방식입니다.

배치를 통해 주기적으로 추천/비추천을 최신화하므로 다음과 같은 장단점을 가지게 됩니다.

  • 장점
    • 사용자에게 보여주기 위한 추천/비추천 개수를 처리하기 위해 락 연산이 필요하지 않습니다.
  • 단점
    • 실시간 추천/비추천 개수를 확인할 수 없습니다.
    • batch로 값을 최신화하기 전, 서버가 다양한 이유로 죽는다면 데이터가 일부 누락될 가능성이 존재합니다.

실시간으로 추천/비추천 개수를 확인할 수 없다는 것은 큰 문제가 아니지만, 데이터가 누락되는 경우 사용자가 이상하다는 느낌을 받을 수 있습니다.

물론 추천/비추천이 핵심 기능은 아니기는 하지만, 그래도 문제가 발생할 수 있으니 최대한 피하는 것이 좋다고 생각합니다.

3. redis에서 추천/비추천을 카운팅

애플리케이션 In-Memory에서 카운팅하던 것을 redis로 변경한 방법입니다.

장단점은 다음과 같습니다.

  • 장점
    • 애플리케이션 서버가 죽더라도 데이터가 누락되지 않습니다.
    • 락 연산이 필요하지 않습니다.
  • 단점
    • 실시간 추천/비추천 개수를 확인할 수 없습니다.
    • redis에 무한히 많은 데이터가 쌓일 수 있습니다.

조회수에서 언급한 대로 2번의 문제를 해결할 수 있는 간단한 방법이지만, 자칫하면 비용 측면에서 매우 위험한 선택이 될 수 있습니다.

4. 랭킹에 등록된 탭 페이지의 추천/비추천에 대해서만 redis에 카운팅, 일반적인 탭 페이지의 경우 DB에 업데이트

3번의 redis에 무한히 많은 데이터가 쌓이는 것을 방지한 방법으로, 다음과 같은 추천/비추천 이벤트 발생 추측을 기반으로 고려한 방법입니다.

  • 일반적인 탭 페이지는 추천/비추천 이벤트가 드물게 발생합니다.
  • 최근에 랭킹에 등록된 탭 페이지는 짧은 시간동안 여러 번의 추천/비추천 이벤트가 발생할 수 있습니다.

랭킹에 등록되지 않았다면 추천/비추천 이벤트가 드물게 발생할 것이므로 DB에 바로 업데이트를 해도 큰 문제가 발생하지 않는다고 판단할 수 있습니다.

추천/비추천 이벤트가 자주 발생할 수 있는, 최근에 링캥에 등록된 탭 페이지의 추천/비추천 값을 redis로 분리해 락이 발생하는 횟수를 줄이고자 한 방법입니다.

장단점은 다음과 같습니다.

  • 장점
    • redis에 저장되는 데이터를 랭킹에 등록된 탭 페이지로 한정지을 수 있습니다.
  • 단점
    • 랭킹에 등록되지 않은 일반적인 탭 페이지에서 추천/비추천 이벤트가 급증하는 경우에 대응할 수 없습니다.
    • 랭킹에 등록된 모든 탭 페이지가 redis에 저장됩니다.

5. 랭킹에 등록된 최근 탭 페이지의 추천/비추천에 대해서만 redis에 카운팅, 일반적인 탭 페이지의 경우 DB에 업데이트

4번의 랭킹에 등록된 모든 탭 페이지가 redis에 저장되는 것을 방지한 방법으로, 다음과 같은 추천/비추천 이벤트 발생 추측을 기반으로 고려한 방법입니다.

  • 최근에 랭킹에 등록된 탭 페이지는 짧은 시간동안 여러 번의 추천/비추천 이벤트가 발생할 수 있습니다.
  • 과거의 랭킹 탭 페이지의 경우 추천/비추천 이벤트가 드물게 발생할 수 있습니다.

랭킹에 등록된 탭 페이지라고 하더라도 등록된지 오래 되었다면 추천/비추천 이벤트가 드물게 발생합니다.
추천/비추친 이벤트가 자주 발생하는 경우는 최근에 랭킹에 등록된 탭 페이지이기 때문에, 이를 분리했습니다.

장단점은 다음과 같습니다.

  • 장점
    • redis에 저장되는 데이터를 랭킹에 등록된 최근 탭 페이지로 한정지을 수 있습니다.
  • 단점
    • 랭킹에 등록된 최근 탭 페이지를 제외한 나머지 탭 페이지에서 조회 이벤트가 급증하는 경우에 대응할 수 없습니다.
    • 스케일 아웃이 어렵습니다.

3 ~ 5번 모두 write-back 방식이므로 write-throught 방식에 비해 성능이 좋지만 스케일 아웃이 어렵습니다.

6. 랭킹에 등록된 최근 탭 페이지의 추천/비추천에 대해서만 redis에 카운팅하면서 동시에 DB에 업데이트, 일반적인 탭 페이지의 경우 DB에 업데이트

write-back 방식인 5번에서 write-throught로 변경했습니다.

장단점은 다음과 같습니다.

  • 장점
    • 스케일 아웃이 쉽습니다.
  • 단점
    • 5번 방식에 비해 비효율적입니다.

결론

조회수와 동일한 결론입니다.

스케일 아웃을 고려한다면 write-throught 방식의 6번이 적합할 것입니다.
하지만 토이 프로젝트를 진행하는데 스케일 아웃을 적용할 비용이 없기 때문에, 5번을 선택하기로 했습니다.

구체적인 내용

5번을 선택하게 되었으니, 조금 더 구체적인 내용을 정리해보고자 합니다.

집계용 추천/비추천 값 관리

조회수와는 다르게, 추천/비추천은 어떤 사용자가 어떤 탭 페이지에 추천/비추천을 했는지 추적할 수 있어야 합니다.

또한, 무조건 상승하는 조회수와는 달리 추천/비추천은 그 호출 api에 따라 결과값이 변경됩니다.

그러므로 로그를 통해 별도로 집계용 추천/비추천 값을 관리하기에는 비효율적일 것입니다.
이를 해결하기 위해, redis에서 값을 컨트롤하기로 결정했습니다.

redis 자료 구조

Set

Set은 중복을 허용하지 않는 자료 구조입니다.

탭 페이지의 추천/비추천 수를 카운팅하기 위해 사용합니다.

key는 탭 페이지의 식별자이고, value로 추천/비추천 수를 저장합니다.

Hash

Hash는 field-value의 값을 갖는 Hash Table 자료 구조입니다.

특정 탭 페이지에 어떤 사용자가 추천/비추천을 했는지 추적하기 위해 사용합니다.

key는 탭 페이지의 식별자이며, field는 추천/비추천을 한 사용자의 식별자(= id), value는 추천/비추천 여부를 저장하게 됩니다.

사용자가 탭 페이지를 추천/비추천했는지 해당 Hash에서 체크한 뒤, 그 값을 제어하게 됩니다.

Bitmaps

Bitmaps는 String에 Binary Operation을 적용한 것으로, 적은 메모리로 Binary 상태값을 적용할 수 있는 비트 지향 연산 집합입니다.

현재 구조는 사용자가 어떤 탭 페이지를 추천/비추천 이벤트를 발생시켰는지 모두 redis에 저장해야 합니다.
그래서 최근에 랭킹에 등록된 탭 페이지의 경우 추천/비추천 이벤트가 많이 발생해 메모리가 낭비될 가능성이 큽니다.

Bitmaps의 경우 10억 개의 값을 표현하는데 Set은 약 3.6GB의 메모리가 필요한 반면, Bitmap은 약 125MB만 필요할 정도로 적은 메모리를 사용합니다.

이를 활용한다면, 사용자 수가 많더라도 어떤 사용자가 추천/비추천을 했는지 효과적으로 파악할 수 있다고 판단했습니다.

특정 사용자에 대한 추천/비추천에 해당하는 Bitmaps를 만든 뒤, 게시글의 식별자(= id)가 0이라면 추천/비추천을 하지 않은 것이고, 1이라면 추천/비추천을 한 것이라고 파악할 수 있습니다.

이러한 방법이 가능한 이유는, 사용자 식별자와 탭 페이지 식별자 모두 DB에서 기본적으로 생성해주는 정수형 PK를 사용하고자 하기 때문입니다.

결론

Bitmaps을 사용하는 경우 redis 메모리를 효율적으로 사용할 수 있지만, 추후 사용자가 추천/비추천한 탭 페이지에 대해 DB에 업데이트할 때 추가적인 처리가 필요합니다.

Hash를 사용하는 경우 Bitmaps처럼 별도의 처리가 필요 없지만, Bitmaps보다는 메모리 효율이 떨어집니다.

메모리 사용량을 고려하면 좋겠지만, 추가적인 관리 포인트가 생기는 것은 현재 상황에서는 굳이 고려할 필요가 없을 것이라 판단해 다음과 같은 자료 구조를 사용하기로 결정했습니다.

  • Set
    • 탭 페이지의 추천/비추천 수치를 관리하기 위한 자료 구조입니다.
  • Hash
    • 사용자의 추천/비추천 여부를 관리하기 위한 자료 구조입니다.

동작 과정

구현 아이디어 5번의 동작 과정은 다음과 같습니다.

  1. 탭 페이지 조회 시 redis에서 해당 탭 페이지의 식별자로 추천/비추천 값 조회
  2. redis에 추천/비추천이 없는 경우, 일반적인 탭 페이지 혹은 등록된지 오래된 랭킹 탭 페이지이므로 DB에서 조회한 뒤 DB에 카운팅
  3. redis에 추천/비추천이 있는 경우, 최근에 등록된 랭킹 탭 페이지이므로 redis에서 조회한 뒤 redis에 카운팅
  4. 정해진 주기에 따라 batch로 redis에 변경된 추천/비추천을 DB에 업데이트
    4-1. DB에 업데이트를 하는 경우, 데이터 누락 및 정합성을 위해 락 적용
  5. 정해진 주기에 따라 batch로 처리한 redis의 값 삭제
    5-1. 정해진 주기에 따라 batch가 동작했다는 것은 redis에 저장된 탭 페이지가 자주 조회되지 않는, 랭킹에 등록된지 오래 된 탭 페이지가 되었음을 의미하기 때문

앞으로의 고민

추천/비추천에 관한 내용을 얼추 마무리했지만, 다음과 같은 항목들도 추후 고민해볼 수 있을 것입니다.

  • 긴 주기(랭킹 집계 시간 주기)를 기준으로 하지 않고 짧은 시간 단위 주기로 랭킹을 집계하는 방식으로 확장할 수 있을지
    • 이에 맞춰 추천/비추천 기능의 최적화도 어떤 식으로 적용할지
  • 구현 방식 아이디어 6번보다 더 효율적인, 스케일 아웃을 적용할 수 있는 아이디어가 있는지
profile
안녕하세요

0개의 댓글