[Redis] 좋아요 수 리팩토링, 랭킹 구현하기

Nicky·2024년 3월 17일
post-thumbnail

문제 상황

지난 포스팅에서 홈 페이지의 게시물 Page들을 캐싱을 통해 저장하는 방법에 대해 다뤘다. 하지만 미처 생각하지 못한 문제가 발생하였는데..

게시물 상세 페이지에서 좋아요를 눌러도 홈페이지에 방영이 안되는 것이었다..

당연히 게시물 좋아요 메서드에는 캐시 무효화가 적용되지 않았기에 캐시된
게시물 상태를 사용했기 때문이다. 하지만 그렇다고 게시물 1개의 상태변화 때문에 전체 페이지에 대한 캐시 무효화를 적용할 수도 없는 노릇이었다.

    @Override
    @Transactional
    public void likePost(String username, Long postId) {
        Post post = findPostByPostId(postId);
        Member currentMember = memberService.findMember(username);
        postLikeService.likePost(post, currentMember);
    }

고민 끝에 좋아요 카운트를 기존의 PostResponse DTO에서 분리하고,

// 게시물 응답
public record PostResponse(Long postId, String authorName, List<String> photoUrls, String location,
                           Double pricePerNight, Integer likeCount) implements Serializable {
    public PostResponse {
    }
}

Post 엔티티의 필드에서 Redis의 key로 관리하기로 하였다.

@Column(name = "like_count")
private Integer likeCount;

코드 리팩토링

게시물의 좋아요 수를 관리할 새로운 RedisService 코드를 구현해 주었다.이제 게시물 생성 시 좋아요 카운트를 0으로 초기화 해주고, 버튼의 누름에 따라 값이 변할 것이다.

PostLikeCountService

@Service
@RequiredArgsConstructor
public class PostLikeCountService {

    private final StringRedisTemplate redisTemplate;

    // 좋아요 수 초기화
    public void initCount(Long postId) {
        redisTemplate.opsForValue().set("postLikeCount:" + postId, String.valueOf(0));
    }

    // 좋아요 수 증가
    public void increaseCount(Long postId) {
        redisTemplate.opsForValue().increment("postLikeCount:" + postId);
    }

    // 좋아요 수 감소
    public void decreaseCount(Long postId) {
        redisTemplate.opsForValue().decrement("postLikeCount:" + postId);
    }

    // 게시물 좋아요 수 조회
    public int getCount(Long postId) {
        String count = redisTemplate.opsForValue().get("postLikeCount:" + postId);
        return Integer.parseInt(count);
    }
    
}

PostController

likePost

    @Override
    @Transactional
    public void likePost(String username, Long postId) {
        Post post = findPostByPostId(postId);
        Member currentMember = memberService.findMember(username);
        postLikeService.likePost(post, currentMember);
        postLikeCountService.increaseCount(postId);
    }

unlikePost

    @Override
    @Transactional
    public void unlikePost(String username, Long postId) {
        Post post = findPostByPostId(postId);
        Member currentMember = memberService.findMember(username);
        postLikeService.unlikePost(post, currentMember);
        postLikeCountService.decreaseCount(postId);
    }

getLikeCount

    // 게시물 좋아요 수
    @GetMapping("/{postId}/like/count")
    public PostLikeCountResponse getLikeCount(@Valid @PathVariable("postId") Long postId) {
        Integer likeCount = postLikeCountService.getCount(postId);
        return new PostLikeCountResponse(likeCount);
    }

좋아요 랭킹 구현

Redis는 빠른 처리 속도와 MySQL DB에 가해지는 부담을 줄여주는 장점 외에도, 다양한 데이터 구조를 지원한다는 큰 이점이 있다. Redis의 Sorted Set을 활용하면, 간단하고 효율적으로 랭킹 시스템을 구축할 수 있다.

Sorted Set은 자동 정렬 기능이 있어, 점수(likeCount)가 추가가 된다면 자동으로 정렬시켜준다.

PostLikeCountService

@Service
@RequiredArgsConstructor
public class PostLikeCountService {

    private final StringRedisTemplate redisTemplate;

    // 좋아요 수 초기화
    public void initCount(Long postId) {
        redisTemplate.opsForValue().set("postLikeCount:" + postId, String.valueOf(0));
        redisTemplate.opsForZSet().add("postLikeRanking", String.valueOf(postId), 0);
    }

    // 좋아요 수 증가
    public void increaseCount(Long postId) {
        redisTemplate.opsForValue().increment("postLikeCount:" + postId);
        redisTemplate.opsForZSet().incrementScore("postLikeRanking", String.valueOf(postId), 1);
    }

    // 좋아요 수 감소
    public void decreaseCount(Long postId) {
        redisTemplate.opsForValue().decrement("postLikeCount:" + postId);
        redisTemplate.opsForZSet().incrementScore("postLikeRanking", String.valueOf(postId), -1);
    }

    // 게시물 좋아요 수 조회
    public int getCount(Long postId) {
        String count = redisTemplate.opsForValue().get("postLikeCount:" + postId);
        return Integer.parseInt(count);
    }

    // 게시물 좋아요 순위 조회
    public List<Long> getRanking(Pageable pageable) {
        int pageNumber = pageable.getPageNumber();
        int pageSize = pageable.getPageSize();

        long start = pageNumber * pageSize;
        long end = start + pageSize - 1;

        // 지정된 범위에 해당하는 게시물 ID 조회
        return redisTemplate.opsForZSet().reverseRange("postLikeRanking", start, end)
                .stream()
                .map(Long::parseLong)
                .collect(Collectors.toList());
    }

}

PostService

    // 게시물 인기순 DTO Page 반환
    @Override
    public Page<PostResponse> getLikeRanking(Pageable pageable) {
        List<Long> postIds = postLikeCountService.getRanking(pageable);
        List<Post> posts = postRepository.findAllByPostIdIn(postIds);
        // 순서 정리
        Page<Post> postPage = new PageImpl<>(postIds.stream()
                .map(postId -> posts.stream()
                        .filter(post -> post.getPostId().equals(postId))
                        .findFirst()
                        .orElse(null))
                .collect(Collectors.toList()));
        return postPage.map(postResponseMapper::toPostResponse);
    }

좋아요 랭킹 조회

현재 각 게시물의 좋아요 수는 다음과 같다.

redis-cli를 통해 조회해보면..

이런 결과가 나오게 되는데, 순서대로 [postId, likeCount]라고 이해하면 된다.

인기순 정렬

서비스 로직에 따라 인기순 정렬에 성공하였다!

profile
코딩 연구소

0개의 댓글