728x90
320x100
🎯 오늘의 목표
- 좋아요 순 피드 조회 (주간/월간/연간/누적)
⚙️ 진행한 작업
- 좋아요 순 피드 조회 (주간/월간/연간/누적)
📌 좋아요 순 피드 조회하는 방법
1️⃣ DB 직접 카운트 + 정렬
- 방식: SELECT * FROM feed ORDER BY like_count DESC
- 장점:
- 간단하고 JPA, QueryDSL로 쉽게 구현 가능
- 별도 인프라(Redis) 없이 DB만으로 해결
- 단점:
- 피드가 많아질수록 ORDER BY like_count DESC 쿼리 부담 커짐
- 실시간으로 TOP N 조회할 때 성능 이슈 발생
- 좋아요수가 자주 변하면 정렬 부담 증가
2️⃣ Like 테이블 집계 쿼리 + 정렬
- 방식 : SELECT feed_id, COUNT(*) FROM like GROUP BY feed_id ORDER BY COUNT(*) DESC
- 장점:
- Feed 엔티티에 like_count 컬럼 안 넣어도 됨
- 집계 쿼리로 동작
- 단점:
- 여전히 매 요청마다 COUNT 계산 필요
- 대량 데이터에서 성능 불안정
3️⃣ Redis Sorted Set(ZSet) 활용
- 방식: 좋아요 발생 시 ZINCRBY로 Redis에 count 업데이트 → 조회 시 ZREVRANGE로 TOP N 조회
- 장점:
- 실시간 카운트 + 랭킹 관리 가능
- DB 부하 최소화, 매우 빠른 조회
- 주간/월간/연간/누적 랭킹 확장 가능
- 단점:
- Redis 설치·운영 필요
- DB와 데이터 싱크 관리 필요 (초기화, 동기화 고려)
- 코드 복잡도 약간 증가
나는 Redis Sorted Set(ZSet) 방식을 선택함
- 이유:
- 좋아요순, 주간·월간·누적 랭킹까지 확장할 계획
- QueryDSL만으로는 대용량 처리 성능에 한계가 있어서 Redis로 실시간 카운트를 관리하고 싶음
- 실무에서 Redis 랭킹 시스템은 매우 많이 사용되므로 학습 경험이 필요함
- Kafka로 확장할 계획이 있으므로 Redis 기반부터 잡아두는 게 유리함
🛠️ 개발내용
📌 RankingFeedService.java 생성
package ddururi.bookbookclub.domain.feed.service;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
/**
* 피드 랭킹 서비스 (좋아요순, 조회수순 관리)
*/
@Service
@RequiredArgsConstructor
public class RankingFeedService {
private final RedisTemplate<String, String> redisTemplate;
/**
* 좋아요 카운트 증가/감소
*
* @param period weekly, monthly, yearly, total
* @param feedId 피드 ID
* @param increment 증가(+1), 감소(-1)
*/
public void incrementLike(String period, Long feedId, int increment) {
String redisKey = getLikeKey(period);
redisTemplate.opsForZSet().incrementScore(redisKey, feedId.toString(), increment);
}
/**
* 좋아요 TOP N 피드 조회
*/
public List<Long> getTopLikedFeeds(String period, int topN) {
String redisKey = getLikeKey(period);
Set<String> feedIds = redisTemplate.opsForZSet()
.reverseRange(redisKey, 0, topN - 1);
return feedIds.stream().map(Long::valueOf).toList();
}
/**
* Redis 키 매핑
*/
private String getLikeKey(String period) {
return switch (period.toLowerCase()) {
case "weekly" -> "popular:feed:like:weekly";
case "monthly" -> "popular:feed:like:monthly";
case "yearly" -> "popular:feed:like:yearly";
case "total" -> "popular:feed:like:total";
default -> throw new IllegalArgumentException("Invalid period: " + period);
};
}
}
📌 LikeService.java 수정
package ddururi.bookbookclub.domain.like.service;
/**
* 좋아요(Like) 관련 비즈니스 로직 처리 서비스
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LikeService {
private final LikeRepository likeRepository;
private final FeedRepository feedRepository;
private final RankingFeedService rankingFeedService;
/**
* 좋아요 토글 기능
* - 이미 좋아요 되어 있으면 삭제 (취소)
* - 좋아요 안 되어 있으면 생성
*
* @param user 사용자
* @param feedId 피드 ID
* @return 토글 후 상태 (true: 좋아요됨, false: 좋아요 취소됨)
*/
@Transactional
public boolean toggleLike(User user, Long feedId) {
// 피드 존재 여부 확인
Feed feed = feedRepository.findById(feedId)
.orElseThrow(() -> new LikeException(ErrorCode.FEED_NOT_FOUND));
// 좋아요가 이미 존재하면 삭제, 없으면 생성
return likeRepository.findByUserIdAndFeedId(user.getId(), feedId)
.map(existingLike -> {
likeRepository.delete(existingLike);
// Redis 좋아요 카운트 -1
rankingFeedService.incrementLike("weekly", feedId, -1);
rankingFeedService.incrementLike("monthly", feedId, -1);
rankingFeedService.incrementLike("yearly", feedId, -1);
rankingFeedService.incrementLike("total", feedId, -1);
return false;
})
.orElseGet(() -> {
likeRepository.save(Like.create(user, feed));
// Redis 좋아요 카운트 +1
rankingFeedService.incrementLike("weekly", feedId, 1);
rankingFeedService.incrementLike("monthly", feedId, 1);
rankingFeedService.incrementLike("yearly", feedId, 1);
rankingFeedService.incrementLike("total", feedId, 1);
return true;
});
}
}
📌 FeedController.java 수정
package ddururi.bookbookclub.domain.feed.controller;
/**
* 피드 관련 API 컨트롤러
* - 작성, 수정, 삭제, 단건 조회, 목록 조회
*/
@RestController
@RequestMapping("/api/feeds")
@RequiredArgsConstructor
public class FeedController {
private final FeedService feedService;
private final RankingFeedService rankingFeedService;
//코드생략//
/**
* 좋아요 랭킹 조회 API
* @param period weekly, monthly, yearly, total
* @param topN 가져올 피드 수 (기본값 10)
* @return 좋아요 TOP N 피드 ID 리스트
*/
@GetMapping("/popular/like")
public ApiResponse<?> getPopularByLike(
@RequestParam String period,
@RequestParam(defaultValue = "10") int topN) {
return ApiResponse.success(rankingFeedService.getTopLikedFeeds(period, topN));
}
}
[포스트맨으로 테스트하기]
GET /api/feeds/popular/like?period=weekly&topN=10
[테스트 결과 : 성공]
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지 #030] 좋아요(Like) 랭킹에서 피드 상세정보 함께 내려주기 (0) | 2025.05.02 |
---|---|
[개발일지 #029] 책(Book) - Spring WebClient로 외부 API 연동: KakaoBookClient 구현 (0) | 2025.05.01 |
[개발일지 #027] 신고(Report) 누적 시, 블라인드 처리 (0) | 2025.05.01 |
[개발일지 #026] 신고(Report) 도메인 구현 및 단위테스트 (0) | 2025.05.01 |
[개발일지 #025] 좋아요(Like) 토글 기능으로 변경하기 (0) | 2025.05.01 |