커뮤니티 플랫폼 프로젝트
[Spring] 좋아요 기능(Redis,RabbitMQ활용)
gotopm
2025. 2. 28. 20:36
좋아요 값을 레디스에 저장하고 조회하고, DB에는 RabbitMQ를 통해 동기화 하는 방식
0.좋아요 엔티티
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Entity
public class Likes extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "postId")
Post post;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "userId")
User user;
}
1.우선 레디스 연결 객체를 만든다.
@Bean
@Qualifier("likes")
public RedisConnectionFactory redisConnectionFactoryForLikes(){
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
-->먼저 레디스의 설정을 담당하는 RediuStandaloneConfiguration 객체 생성
configuration.setHostName(host);
configuration.setPort(port);
configuration.setDatabase(1);
-->설정객체에 레디스서버의 host,port 그리고 몇번 데이터베이스를 사용할 지 설정
return new LettuceConnectionFactory(configuration);
-->RedisConnectionFactory는 인터페이스. 이를 구현하는 LettuceConnetionFactory객체를
리턴하는데, 우리가 만든 설정을 매개변수로 하여 주입.
}
@Bean
@Qualifier("likes")
public RedisTemplate<String,String> redisTemplateforLikes(@Qualifier("likes") RedisConnectionFactory redisConnectionFactoryForLikes){
RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactoryForLikes);
return redisTemplate;
}
-->LettuceConnetionFactory가 레디스와 물리적인 연결을 하는 객체인 반면, 아래
RedisTemplate는 레디스의 데이터를 읽고/쓰는 기능을 제공하는 객체다.
따라서 위에서 만든 매서드로 만들어지는 빈 객체를 매개변수로 받는 것.
2.RabbitMQ를 통해 RDB에 동기화 할 것 이기 때문에 RabbitMQ 연결 객체도 만든다(+RabbitMQ에 생성될 큐도)
@Configuration
public class RabbitmqConfig {
@Value("${spring.rabbitmq.host}")
private String host;
@Value("${spring.rabbitmq.port}")
private int port;
@Value("${spring.rabbitmq.username}")
private String username;
@Value("${spring.rabbitmq.password}")
private String password;
@Value("${spring.rabbitmq.virtual-host}")
private String virtualHost;
--> .yml에 세팅한 Rabbitmq 값들을 불러온다
public static final String BACKUP_QUEUE_AL="backupAddLike";
public static final String BACKUP_QUEUE_ML="backupMinusLike";
--> RabbitMq에서 사용할 큐를 설정하는데 큐의 이름을 상수로 선언
@Bean
public Queue AddlikeQueue(){
return new Queue(BACKUP_QUEUE_AL,true);
}
-->빈으로 등록된 AddlikeQueue()가 실행되면 backupAddLike라는 이름으로 래빗엠큐내에서 큐가 생성됨.
이때 true설정은 RabbitMq가 꺼졋다가 다시 켜져도 큐가 유지되도록 하는 지속성에 대한 설정
@Bean
public Queue MinuslikeQueue(){
return new Queue(BACKUP_QUEUE_ML,true);
-->마찬가지로,MinuslikeQueue()가 실행되면 backupMinusLike라는 이름으로
래빗엠큐내에서 큐가 생성됨.
@Bean
public ConnectionFactory connectionFactory(){
CachingConnectionFactory connectionFactory = new CachingConnectionFactory();
connectionFactory.setHost(host);
connectionFactory.setPort(port);
connectionFactory.setUsername(username);
connectionFactory.setPassword(password);
connectionFactory.setVirtualHost(virtualHost);
return connectionFactory;
}
-->레디스에서 물리적인 연결은 LettuceConnetionFactory객체가 한 것처럼,
래빗엠큐에서는 CachingConnectionFactory객체가 담당한다.
-->레빗엠큐의 설정값들을 바탕으로 물리적인 연결 객체를 만든다.
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
-->객체를 자동으로 JSON으로 직렬화,역직렬화 한다는 뜻
return rabbitTemplate;
}
-->물리적인 연결을 바탕으로 래빗엠큐를 통해 메세지를 주고 받는 RabbitTemplate객체를 만든다.
}
3. 좋아요 및 좋아요 취소 이벤트를 비동기적으로 처리하는 서비스 클래스를 만든다
@Service
@Transactional
public class LikesRabbitmqService {
private final RabbitTemplate template;
private final PostRepository postRepository;
private final UserRepository userRepository;
private final LikesRepository likesRepository;
public LikesRabbitmqService(RabbitTemplate template, PostRepository postRepository, UserRepository userRepository, LikesRepository likesRepository) {
this.template = template;
this.postRepository = postRepository;
this.userRepository = userRepository;
this.likesRepository = likesRepository;
}
public void publishForAdding(BackupForLikesDto dto){
template.convertAndSend(RabbitmqConfig.BACKUP_QUEUE_AL,dto);
}
-->BachupForLikesDto는 userId,PostId를 칼럼으로 가지는 dto로 따로 만들어놨다.
-->converAndSend(큐이름,큐에 넣을 데이터(문자열or객체))는
래빗엠큐에 메시지를 보내는 역할을 하는 메서드
-->RabitListner가 이 큐를 구독하고 있으면 메시지를 받아 처리한다
-->좋아요 추가 요청을 보내는 큐!
@RabbitListener(queues = RabbitmqConfig.BACKUP_QUEUE_AL)
public void subscribeForAdding(Message message) throws JsonProcessingException {
String messgeBody = new String(message.getBody());
메세지의 내용을 가져온다.
ObjectMapper objectMapper = new ObjectMapper();
BackupForLikesDto dto = objectMapper.readValue(messgeBody, BackupForLikesDto.class);
-->메세지에 저장된 JSON문자열을 역직렬화(->객체로 만들어준다)
Post post = postRepository.findById(dto.getPostId()).orElseThrow(()-> new EntityNotFoundException("게시글이 존재하지 않습니다"));
User user = userRepository.findByLoginIdAndDelYN(dto.getUserId(), DelYN.N).orElseThrow(()->new EntityNotFoundException("유저가 존재하지않습니다"));
Likes likes = Likes.builder()
.user(user)
.post(post)
.build();
likesRepository.save(likes);
-->메세지에 저장된 postId와 userId를 통해 좋아요를 만들 수 있다.
}
-->backupAddLike이름의 큐를 구독하고 있는 메서드로 backupAddLike큐에 메시지가
있으면 실행된다
-->큐에서 메세지를 받아 db에 좋아요를 저장하는 것!
public void publishForMinus(BackupForLikesDto dto){
template.convertAndSend(RabbitmqConfig.BACKUP_QUEUE_ML,dto);
}
-->좋아요 취소를 요청하는 큐!
@RabbitListener(queues = RabbitmqConfig.BACKUP_QUEUE_ML)
public void subscribeForMinus(Message message) throws JsonProcessingException {
String messgeBody = new String(message.getBody());
ObjectMapper objectMapper = new ObjectMapper();
BackupForLikesDto dto = objectMapper.readValue(messgeBody, BackupForLikesDto.class);
Post post = postRepository.findById(dto.getPostId()).orElseThrow(()-> new EntityNotFoundException("게시글이 존재하지 않습니다"));
User user = userRepository.findByLoginIdAndDelYN(dto.getUserId(), DelYN.N).orElseThrow(()->new EntityNotFoundException("유저가 존재하지않습니다"));
Likes cancelledLike = likesRepository.findByPost_IdAndUser_Id(post.getId(),user.getId()).orElseThrow(()->new EntityNotFoundException("해당 좋아요가 없습니다."));
likesRepository.delete(cancelledLike);
}
-->큐에서 메시지를 받아 db의 좋아요를 삭제하는 것!
4.좋아요 컨트롤러 단
-좋아요의 특징은 한 사용자가 좋아요를 누르면 +1이 되고, 다시 누르면 -1이 된다는 것이다.
-따라서, 하나의 메서드로 좋아요가 +1이 되고 다시 누르면 -1이 되는 것을 구현한다.
// 1.좋아요 누르기(한번 누르면 좋아요, 다시 누르면 좋아요 해제)
@PostMapping("/add/{id}")
public ResponseEntity<?> addLike(@PathVariable Long id){
Map<String,Object>likeStatus = likesService.toggleLike(id);
return new ResponseEntity<>(new CommonDto(HttpStatus.OK.value(),"toggle succeed",likeStatus),HttpStatus.OK);
}
-->여기서 id는 사용자가 누른 게시글의 id이다.
프론트에 Map형태로 리턴하는 것은 해당 포스트의 좋아요 갯수만 리턴하는 것이 아니라,
이 사용자가 좋아요를 한 상태인지 좋아요를 해제한 상태인지에 대한 정보도 리턴해야하기 때문에
이 2가지를 리턴하기 위해 Map형태로 리턴한다.
*프론트는 이 사용자가 좋아요를 했는지 헤제했는지에 대한 boolean값을 받음으로써,
화면에서 좋아요 아이콘에 변화를 줄 수 있다.
(예를 들어, 한 사용자가 자신이 좋아요를 누른 포스트를 다시 조회하면 그 좋아요 아이콘이 색칠
되어있다는 방식으로 이미 좋아요를 누른 게시물이라는 것을 알려줄 수 있다는 뜻)
5.좋아요 서비스 단
-좋아요 요청이 들어오면 레디스에 저장되는데,
이때 3종류의 key-value값을 저장한다.(예를 들어 id=2인 유저가 id=1인 포스트에 대한 좋아요가 들어오면,)
- 키가 post-1-likeUsers, value는 1번 게시글에 좋아요를 누른 사람들이다.
- 키가 post-1-likeCount, value는 1번 게시글에 대한 좋아요 개수
- 키가 user-2-myLikeList, value는 2번 유저가 좋아요한 게시글들이다.
2번 키는 해당 게시물에 대한 좋아요 개수에 1을 더하거나 빼야하므로 당연히 있어야할 키.
1번 키는 해당 게시물에 좋아요한 사람목록에서 요청을 한 유저가 있다면 이미 좋아요를 눌렀다는 걸 알 수 있으므로 -1을 처리하게 하고, 목록에서 없는 유저라면 좋아요+1을 처리하게 하기 위해 존재하는 키다.
이때 value는 중복이 불가능한 set으로 설정함으로써 한 사용자가 중복해서 좋아요+1 하는 것을 막는다
3번 키는 나중에 유저가 좋아요한 목록을 볼 수 있게 하기 위해 만든 키다.
public Map<String, Object> toggleLike(Long postId){
String loginId = SecurityContextHolder.getContext().getAuthentication().getName();
User loginUser = userRepository.findByLoginIdAndDelYN(loginId, DelYN.N).orElseThrow(()->new EntityNotFoundException("없는 사용자입니다"));
Post post = postRepository.findById(postId).orElseThrow(()-> new EntityNotFoundException("없는 게시글입니다"));
-->Authentication 객체를 통해 현재 로그인한 user를 불러온다.매개변수로 받은 post도 불러온다
어떤 유저가 어떤 포스트에 대해 좋아요한건지 확인 또는 저장해야하니까.
BackupForLikesDto dto = BackupForLikesDto.builder().PostId(postId).UserId(loginId).build();//레디스 mq에 들어갈 백업용dto
-->RabbitMQ를 통해 db에 백업을 하기 위해 만드는 dto객체
String likeUserKey = "post-" + postId +"-likeUsers"; //레디스에 해당 포스트에 좋아요 누른 목록사람에 대해 저장될 키값(밸류는 셋 자료구조를 이용한 좋아요 누른 사람 목록) ex.post-1-likeusers : 5 이렇게 저장될 수 있도록
String likeCountKey = "post-" + postId + "-likeCount"; //레디스에 해당 포스트의 좋아요 개수와 관련한 키값(밸류는 좋아요 개수)
String likeMyListKey = "user-"+ loginId + "-myLikeList"; //레디스에 해당 유저가 좋아요한 포스트id를 저장.그에 대한 키값
if(redisTemplate.opsForSet().isMember(likeUserKey,loginUser.getLoginId())){ //해당 포스트 좋아요 누른 유저 목록에 지금 유저 아이디가 있다면, 즉 이미 좋아요를 누른사람이라면
redisTemplate.opsForSet().remove(likeUserKey,loginUser.getLoginId()); //해당 목록에서 현재 유저를 제거
redisTemplate.opsForValue().decrement(likeCountKey);//그리고 좋아요 개수에서 1을 뺀다
redisTemplate.opsForSet().remove(likeMyListKey,String.valueOf(post.getId()));//내가 좋아요 한 글 목록에 해당 포스트 아이디를 삭제한다.
likesRabbitmqService.publishForMinus(dto);//rbd에서 좋아요 삭제
} else{
redisTemplate.opsForSet().add(likeUserKey,loginUser.getLoginId());//해당 포스트 좋아요 누른 유저목록에 현재 유저를 추가
redisTemplate.opsForValue().increment(likeCountKey);//그리고 좋아요 개수를 1증가시킨다
redisTemplate.opsForSet().add(likeMyListKey,String.valueOf(post.getId()));//내가 좋아요 한 글 목록에 해당 포스트 아이디를 저장시킨다.
likesRabbitmqService.publishForAdding(dto); //rdb에서 좋아요 추가
}
boolean liked = redisTemplate.opsForSet().isMember(likeUserKey,loginUser.getLoginId()); // 해당 유저가 좋아요 눌렀는지 아닌 지 불린값
Map<String,Object> likeStatus = new HashMap<>();
likeStatus.put("likesCount",Long.parseLong(redisTemplate.opsForValue().get(likeCountKey)));
likeStatus.put("liked",liked);
return likeStatus;
}