조회수에 대한 고민

appti·2024년 3월 27일
0

고민

목록 보기
2/3

서론

탭 관련 토이 프로젝트를 진행할 때, 핵심이 되는 도메인에서 간단할 수 있겠지만 가장 최적화를 할 수 있을만한 부분이 바로 조회수라고 생각합니다.

이 부분에 대해서 어떤 식으로 최적화를 진행할 수 있을지 고민해보고 적용해보고자 합니다.

특징

토이 프로젝트에서의 조회수 특징은 다음과 같습니다.

  • 조회수는 탭 요소마다 적용되는 것이 아닌, 오로지 탭 페이지에만 적용됩니다.
  • 실시간으로 정확하게 일치할 필요는 없습니다.
  • 조회수를 집계한 결과로 랭킹을 적용해야 합니다.
  • 중복 조회는 허용됩니다.

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

  • 해당 서비스는 개인이 학습하면서 참고했던 탭을 태그별로 정리하는 기능을 제공합니다.
    • 작성한지 오래된 탭 페이지는 해당 페이지를 올린 당사자가 아니라면 조회 이벤트가 발생할 일은 거의 없습니다.
    • 중복 조회가 허용되지만, 한 사용자가 동일한 탭 페이지를 짧은 시간동안 여러 번 조회 이벤트가 발생할 일은 거의 없습니다.
    • 최근에 랭킹에 등록된 탭 페이지는 짧은 시간동안 여러 번 조회 이벤트가 발생할 가능성이 매우 큽니다.
    • 등록된지 오래된 랭킹 탭 페이지의 경우 조회 이벤트가 발생할 일은 거의 없습니다.

문제

일반적으로 조회수에서 문제가 발생하는 부분은 바로 업데이트입니다.

서비스 특성상 중복 조회가 가능하기 때문에 상당히 많은 조회수 업데이트 api가 호출될 것입니다.
이 때 마다 애플리케이션에서 DB로 요청을 보내고, DB에서 조회수의 일관성을 위해 락을 걸며 업데이트 하는 과정으로 인해 비용이 소모될 것입니다.

즉, 조회수에서 가장 큰 비용이 소모되는 것은 락이라는 것입니다.
또한 락으로 인해 비용 뿐만 아니라 지연과 관련해 문제가 발생할 수도 있습니다.

이러한 락을 어떤 방식을 활용해 줄일지가 핵심이라고 판단했습니다.

개선 아이디어

조회수 특징 중 다음과 같은 항목이 있습니다.

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

실시간으로 조회수가 정확히 일치할 필요가 없다는 의미는, 사용자에게 조회수가 크게 의미를 가지는 데이터가 아니라는 의미입니다.
사실 사용자 입장에서는 조회수가 누락되었다고 한들, 조회수가 증가하기만 해도 문제를 느끼지 못합니다.

그러므로 조회수를 집계할 필요가 없는 경우 조회수의 정확성은 필요가 없는 것이며, 락을 사용할 필요도 없게 됩니다.

문제는 조회수를 집계해 랭킹을 구해야 한다는 점입니다.
이는 사용자가 조회할 수 있는 조회수와는 다르게, 조회수의 정확한 값이 필요합니다.

즉 조회수라는 데이터에 대해 탭 페이지의 조회 시의 기능과 탭 페이지의 조회수 랭킹 기능의 불일치로 인해 발생했다고 판단했습니다.

이 기능의 불일치를 어떤 식으로든지 해결한다면 긍정적인 결과를 얻을 수 있을 것이라고 생각했습니다.

구현 방식 아이디어

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

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

1. 조회 시마다 DB에 업데이트

조회수를 집계한 결과로 랭킹을 적용해야 하기 때문에, 이 방식에서는 데이터의 정합성을 고려할 수 밖에 없습니다.

그러므로 조회 이벤트가 발생해 조회수를 1씩 증가시키기 위해 락을 설정할 수 밖에 없고, 이는 문제에서 언급한 바와 그대로 매우 비효율적인 작업이 될 것입니다.

대신 구현이 간단하고 직관적이기 때문에 규모가 작은, 지금 진행하고 있는 토이 프로젝트에 적합하다고 볼 수 있습니다.

2. 애플리케이션 In-Memory에 조회수를 카운팅

애플리케이션에 조회수를 카운팅하고, 그 카운팅한 결과를 일정 주기에 따라 DB에 flush를 하는 방식입니다.

배치를 통해 주기적으로 조회수를 최신화해야 하기 때문에 다음과 같은 장단점을 가지게 됩니다.

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

단점 모두 현재 서비스에서 조회수가 크게 중요한 기능이 아니기 때문에 큰 상관이 없는 상태입니다.

사실 지금 수준에서 진행할 토이 프로젝트에서는 가장 적합한 방식이라고 볼 수 있습니다.

3. redis에서 조회수를 카운팅

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

별도의 batch 작업을 통해 데이터를 동기화합니다.

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

  • 장점
    • 사용자에게 보여주기 위한 조회수를 처리하기 위해 락 연산이 필요하지 않습니다.
    • 애플리케이션 서버가 죽더라도 데이터가 누락되지 않습니다.
  • 단점
    • 실시간 조회수를 확인할 수 없습니다.
    • redis에 무한히 많은 데이터가 쌓일 수 있습니다.

단점 중 redis에 무한히 많은 데이터가 쌓이는 경우가 치명적이라고 볼 수 있습니다.

만약 트래픽이 많아 redis에 무한히 많은 데이터가 쌓인다면, 비용을 감당할 수 없기 때문에 다른 방법을 고려해야 합니다.

만약 트래픽이 없어 redis에 큰 데이터가 쌓이지 않았다면, 굳이 redis를 쓰지 않아도 애플리케이션의 In-Memory만으로도 충분할 것입니다.
즉, 이 경우 애플리케이션으로 충분한데 굳이 redis를 추가해 관리 포인트만 추가된 경우라고 볼 수 있습니다.

2번의 문제점을 가장 간단하게 해결할 수 있는 방법이지만, 큰 고민 없이 단순하게 도입한다면 최악의 선택이 될 수 있다고 생각합니다.

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

조회수 발생 이벤트를 예측한 내용 중, 다음과 같은 내용이 있었습니다.

  • 작성한지 오래된 탭 페이지는 해당 페이지를 올린 당사자가 아니라면 조회 이벤트가 발생할 일은 거의 없습니다.
  • 최근에 랭킹에 등록된 탭 페이지는 짧은 시간동안 여러 번 조회 이벤트가 발생할 가능성이 매우 큽니다.

잦은 조회 이벤트로 인해 DB 락이 많이 발생하는 경우는 랭킹에 올라온 글 페이지만 해당되며, 다른 일반적인 글은 작성자만 조회한다고 예상할 수 있습니다.

그렇다면 작성자만 조회할 것이라고 추측할 수 있는 일반적인 탭 페이지는 기본적으로 DB에서 업데이트를 수행하고, 조회 이벤트가 많이 발생할 것이라고 판단되는 랭킹에 등록된 탭 페이지만을 redis에서 관리하는 방식입니다.

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

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

이 정도 개선했다면, redis를 적용하는 이유 중 일부가 될 수 있다고 생각합니다.

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

조회수 발생 이벤트를 예측한 내용 중, 다음과 같은 내용이 있었습니다.

  • 등록된지 오래된 랭킹 탭 페이지의 경우 조회 이벤트가 발생할 일은 거의 없습니다.

즉, 4번의 단점인 랭킹에 등록된 모든 탭 페이지를 redis에 저장하지 않고, 랭킹에 등록된 최근 탭 페이지만을 등록하면 이를 개선할 수 있습니다.

해당 방법에 대한 장단점은 다음과 같습니다.

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

단점에 스케일 아웃이 포함되었는데, 사실 이 방법은 redis를 언급했던 모든 방법이 가지고 있는 단점입니다.

여태까지는 redis에 조회수를 기록했다가 나중에 처리하는 write-back 방식을 사용했기 때문에 자연스럽게 스케일 아웃이 어려워지는 상황이었습니다.

그렇다면 이를 해결하기 위해서는 write-throught 방식을 사용하면 될 것입니다.

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

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

해당 방법에 대한 장단점은 다음과 같습니다.

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

결론

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

구체적인 내용

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

집계용 조회수 관리

3번부터 5번까지는 모두 탭 페이지 조회 기능 / 탭 페이지 랭킹 집계 기능 중 탭 페이지 조회 기능에 집중한 방식입니다.

랭킹 집계를 위해 별도의 처리가 필요한 상황입니다.

이 경우, 정확한 조회수를 얻기 위해 로그를 활용할 수 있습니다.

Nginx를 활용하거나, AOP 혹은 데코레이터 패턴 등을 활용해 로그를 출력하고 이 로그를 직접 카운팅하는 방식으로 정밀한 조회수를 얻을 수 있을 것입니다.

조회수를 엄격하게 구분하려면 요청이 성공해 200 OK가 응답되는 그 순간을 기준을 로그로 처리하면 될 것이고, 이 경우 Nginx를 통해 처리하는 것이 편할 것입니다.

그게 아니라면 조금 더 간단하게 애플리케이션 레벨에서 로그를 출력하거나 그라파나, 핀포인트 등을 활용할 수도 있을 것입니다.

redis 자료 구조

Hash

조회수를 사용할 때, 가장 먼저 떠올릴 수 있는 자료 구조는 Hash입니다.
key로는 탭 페이지 식별자를, value로는 조회수를 카운팅할 수 있습니다.

다만 이전 단계에서 집계용 조회수 관리를 로그를 통해 관리하기로 결정했으므로, redis는 사용자에게 보여주기 위한 조회수(= 정확하지 않아도 아무 상관 없는 값)임을 의미합니다.

어처피 별도로 집계용 조회수를 계산할거라면 정밀한 Hash가 아닌, redis의 확률형 자료 구조만으로도 충분할 것입니다.

BloomFilter

BloomFilter는 확률형 자료 구조로, 집합에 특정 요소가 속해있는지를 판단하는 자료 구조입니다.

집합에 특정 요소가 속해있는지를 판단하는 자료 구조입니다.
이후에 비교할 Hyperloglog보다 메모리 효율이 뛰어납니다.

다만 목적 자체가 조회수와 같이 개수와 관련된 것이 아니라, 특정 요소가 속해있는지를 판단하는 것이기 때문에 잘 안어울린다고도 볼 수 있습니다.

또한 거짓 긍정이 발생할 수 있으므로, 요구 사항과는 거리가 있는 자료 구조라고 볼 수 있겠습니다.

Hyperloglog

Hyperloglog는 확률형 자료 구조로, 집합의 고유 요소 개수를 추정하는 자료 구조입니다.

BloomFilter보다는 메모리 효율이 좋지 않지만, 그래도 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개의 댓글