오늘은 레디스를 통해 조회수 로직을 개선하는 작업을 진행했다!
우리 서비스에서는 모임 게시글을 단건 조회할 때 조회수를 카운트하고, 보여주고 있다.
사실 아직은 이 조회수가 사용되는 곳이 없지만, 추후에 고도화 작업을 진행하면서 조회수와 좋아요 수 등을 복합적으로 계산해 인기순 정렬을 만드려고 한다.
기존에 우리는 조회 시에 DB에서 직접 viewCount++를 호출하여 조회수를 반영했는데,
이렇게 구현할 경우에는 조회 로직에서도 update 쿼리가 발생한다는 한계가 있었다.
게다가 동시에 여러명이 접속할 경우, 동시성을 제어하기에도 어려워진다.
그래서 인메모리 기반으로 작동하는 레디스를 통해 이 조회수를 관리해보기로 하였다.
우선, 위와 같이 매일 어떤 모임에 대해 몇 명이 조회했는지를 저장하는 Zset을 생성한다.
하지만, 이 Zset은 매일 새롭게 생성되는 만큼 누적 조회수를 담을 수는 없게 된다.
Long viewCount = meetingCacheService.getViewCount(meetingId).longValue();
meetingDetailResponse.increaseViews(viewCount);
그래서 나는 기존의 값을 레디스에 불러오기보다는 이렇게 DB에 저장되어 있던 조회수에 레디스에서 불러온 조회수를 더해서 보여주도록 구현하였다.
@Scheduled(cron = "0 0 0 * * *")
public void meetingViewSync() {
LocalDate dayBeforeTody = LocalDate.now().minusDays(1);
Set<TypedTuple<String>> allViewList = meetingCacheService.getAllViewList(dayBeforeTody);
for (TypedTuple<String> tuple : allViewList) {
Long meetingId = Long.parseLong(tuple.getValue());
Long viewCount = tuple.getScore().longValue();
increaseViewCount(meetingId, viewCount);
}
}
@Transactional
public void increaseViewCount(Long meetingId, Long viewCount) {
Meeting meeting = meetingRepository.findActivateMeetingById(meetingId);
meeting.increaseViews(viewCount);
meetingRepository.save(meeting);
}
그리고 매일 자정에는 레디스에 저장되어있던 내용들을 DB에 반영하도록 스케줄러를 구현하였다.
DB 자체에서 조회수를 관리할 때는 이런 추가 계산 작업이 필요하지 않았는데,
레디스를 도입하는 순간 기존의 조회수와 현재 조회수를 맞추기 위한 추가 계산도 필요하고, 레디스 장애에 대응하기도 해야한다.
그래도 레디스를 사용할 때의 성능 개선적인 측면이나, 불필요한 쿼리를 생성하지 않기 위해서 레디스를 도입하는 것이 낫겠다고 판단하였다.
여기에 추가로 조회수를 올리기 위해 새로고침을 계속 하는 행위, 즉 어뷰징을 방지하기 위한 작업을 진행하였다.
우선은 어떤 유저가 어떤 게시글을 조회했는지, 유저의 ID와 모임 게시글의 ID를 기준으로 로그를 기록해두려고 하였다.
하지만, 우리 서비스에서 단건 조회는 로그인된 유저가 아니더라도 접근이 가능하다.
그래서 저렇게 로그를 기록한다면, 의도적으로 로그인을 풀고 조회를 반복할 수 있게 된다.
이런 한계를 보완하기 위해서 유저의 ID 보다는 모든 사용자가 가지고 있는 IP 주소를 통해 로그를 기록하기로 결정했다.
사용자가 요청을 보낼 때, 헤더에 X-Forwarded-For라는 키로 IP 주소를 담아서 보내주게 된다.
그래서 HttpServletRequest 객체에서 IP 주소를 조회한 뒤, 사용자의 IP와 게시글의 ID를 기준으로 로그를 기록하도록 구현하였다.
이때 로그는 레디스에서 분산 락을 구현할 때처럼 Redis의 SET NX 명령어를 사용하였다.
이렇게 모임의 ID와 IP 주소를 키로 저장한 뒤, TTL을 한 시간으로 설정하여서 한 시간 뒤에는 다시 조회해도 조회수가 오르도록 설정하였다.
사실 포스트맨으로 테스트를 해볼 때에는 내가 직접 IP 주소를 넣어야 하기 때문에 제대로 동작하는 것을 확인했지만,
실제 운영 환경에서 사용자의 IP 주소를 제대로 추출할 수 있을지는 아직 모르겠다.
그래서 이 부분은 배포 후 테스트를 통해 조금 더 보완해야 할 것 같다.
우리 팀이 작성한 코드는 깃허브를 통해 업로드해두었다.
GitHub 보러가기
이제 추가적으로 작업할 내용들을 모두 끝냈기 때문에, 내일부터는 엘라스틱 서치를 시도해볼 예정이다!!
엘라스틱 서치를 써보고 싶었는데, 재미있을 것 같아서 기대된다.