스팟에서 게시판 메인에는 ‘BEST 인기글’이라는 기능이 있다. 여기서 인기 점수를 계산할 때 좋아요 수, 조회 수, 댓글 수를 섞어서 계산하는데, 이 중 제일 조작하기 쉬운 게 조회 수다.
지금 구조는 단순하다. 게시글 상세 페이지에 들어오면 무조건 조회수가 +1 된다. 그런데 이러면 누가 새로고침만 눌러도 조회수가 쭉쭉 올라가 버린다. 사실 이건 악용 가능성이 너무 높고, 형평성에도 맞지 않는다. 예를 들어 A라는 글은 사람들이 진짜로 읽어서 조회수가 늘어난 거고, B라는 글은 글쓴이가 새로고침으로 억지로 올려버린 거라면, 둘이 똑같이 취급되는 게 말이 안 된다고 생각했다.
그래서 이번에 리팩토링 하면서 조회 수 어뷰징 방지를 고민하게 됐다.
일단 고민 범위를 줄였다. 스팟은 로그인하지 않은 사용자에게는 애초에 게시판 기능을 제공하지 않는다. 즉, 비로그인 사용자는 조회수를 올릴 수 있는 구조 자체가 아예 없다. 그래서 “로그인한 사용자”에 대해서만 조회수를 어떻게 셀지 신경 쓰면 된다.
“그럼 로그인 한 사용자를 고유하게 식별할 수 있는 값이 뭐가 있을까?”라는 고민이 들었다.
처음에 떠오른 건 JWT 토큰이었다. 토큰 자체를 게시글 ID랑 묶어서 키로 쓰면 한 번 조회한 사용자가 또 조회하는 건 막을 수 있겠다고 생각했다. 근데 문제는 토큰은 재로그인할 때마다 새로 발급된다는 거다. 이러면 사실상 같은 회원인데, 토큰이 달라져서 다른 사람처럼 인식되는 허점이 생긴다. 그래서 토큰은 배제했다.
그 다음 고민은, “애초에 토큰 안에 들어있는 회원 ID를 쓰면 되지 않나?” 였다. 이러면 토큰이 재발급되든 말든, 회원 단위로 조회 여부를 체크할 수 있다. 결국 이를 통해 어뷰징을 막기로 결정했다.
“어떤 회원이 어떤 게시글을 조회했는지”를 기록하는 게 핵심이었다. 그래서 세 가지 방법을 비교했다.
제일 먼저 떠오른 건 그냥 서버 메모리에 Map을 두는 방식이었다.
두 번째는 그냥 DB에 기록하는 방식이었다.
세 번째가 Redis였다.
그래서 RDB와 Redis를 비교하기 위해 약 100,000회 테스트를 진행 했다.
구분 | 응답 평균 시간(ms) | P95(ms) | P99(ms) |
---|---|---|---|
RDB | 108.02482 | 129 | 325 |
Redis | 64.0824 | 87 | 126 |
Redis가 RDB 대비 평균 40% 이상 빨랐고, tail latency도 더 안정적이었다. 또한 Redis는 요청이 몰려도 응답 시간이 일정하게 유지됐는데, RDB는 몰리면 체감 지연이 확 뛰었다.
좋아요 수나 댓글 수 처럼 실제 데이터의 갯수를 저장하는 경우라면 100% 정합성이 중요하다고 판단했겠지만, 조회 수 같은 경우는 정합성 보다는 성능에 더 집중하는 것이 더 옳은 방법이라는 생각이 들었다.
그래서 최종적으로는 Redis를 도입했다.
구현 방식은 다음과 같다.
view:guard:{postId}:{memberId}
즉, 회원이 게시글을 최초 조회할 때만 Redis에 기록되고, 그때만 DB 조회수가 증가한다. 이후 같은 회원이 10분 안에 새로고침을 하든 다시 들어오든 조회수는 안 올라간다. 이후 TTL이 지나면 다시 올라간다.