Skip to content

Commit

Permalink
#125 - 채팅, 채팅방 관련 전체적인 로직 변경 (#135)
Browse files Browse the repository at this point in the history
* refactor: ChatCategory 변경
* refactor: Chat 데이터 100개만 Redis에 저장
- 첫 페이지 조회가 가장 많이 이루어질 것을 예상하여 최신 채팅 데이터 100개만 Redis에 저장한다.
* test: 테스트 코드 변경
* refactor: 채팅방 관련 로직 변경
- ChatUser에 isDeleted 컬럼 추가
- 채팅방 나갈 때 채팅방에 아무도 없을 경우 삭제
- 내 채팅방 목록 조회
  • Loading branch information
Leehunil authored Jan 15, 2025
1 parent c14e633 commit 387d023
Show file tree
Hide file tree
Showing 19 changed files with 222 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,21 @@ public class ChatCustomResponse {

private LocalDateTime lastSendAt;

public static ChatCustomResponse toResponseFromEntity(List<Chat> chats, boolean hasNext,
LocalDateTime nextChatTimeStamp) {
public static ChatCustomResponse toResponseFromEntity(List<Chat> chats, boolean hasNext) {
return new ChatCustomResponse(
chats.stream()
.map(ChatResponse::toResponseFromEntity)
.toList(),
hasNext,
nextChatTimeStamp
null
);
}

public static ChatCustomResponse toResponseFromDto(List<ChatResponse> chats, boolean hasNext,
LocalDateTime nextChatTimeStamp) {
public static ChatCustomResponse toResponseFromDto(List<ChatResponse> chats, boolean hasNext) {
return new ChatCustomResponse(
chats,
hasNext,
nextChatTimeStamp
hasNext ? chats.get(chats.size() - 1).getSendAt() : null
);
}
}
14 changes: 13 additions & 1 deletion src/main/java/com/palettee/chat/domain/ChatUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,21 @@ public class ChatUser {
@JoinColumn(name = "user_id")
private User user;

@Column(name = "is_deleted")
private boolean isDeleted;

@Builder
public ChatUser(ChatRoom chatRoom, User user) {
public ChatUser(ChatRoom chatRoom, User user, boolean isDeleted) {
this.chatRoom = chatRoom;
this.user = user;
this.isDeleted = isDeleted;
}

public void participation() {
this.isDeleted = true;
}

public void leave() {
this.isDeleted = false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,10 @@ public ChatCustomResponse findChatNoOffset(Long chatRoomId, int size, LocalDateT
.fetch();

boolean hasNext = chats.size() > size;

LocalDateTime lastSendAt = null;
if(hasNext) {
if(size != 0) {
lastSendAt = chats.get(size-1).getSendAt();
}
chats = chats.subList(0, size);
}

return ChatCustomResponse.toResponseFromEntity(chats, hasNext, lastSendAt);
return ChatCustomResponse.toResponseFromEntity(chats, hasNext);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.palettee.chat.repository;

import com.palettee.chat.domain.*;
import io.lettuce.core.dynamic.annotation.Param;
import org.springframework.data.jpa.repository.*;

public interface ChatImageRepository
extends JpaRepository<ChatImage, Long> {

public interface ChatImageRepository extends JpaRepository<ChatImage, Long> {
@Modifying
@Query("DELETE FROM ChatImage ci WHERE ci.chat IN (SELECT c FROM Chat c WHERE c.chatRoom.id = :chatRoomId)")
void bulkDeleteChatImagesByChatRoomId(@Param("chatRoomId") Long chatRoomId);
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

import com.palettee.chat.domain.Chat;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface ChatRepository extends JpaRepository<Chat, Long>, ChatCustomRepository {
@Modifying
@Query("delete from Chat c where c.chatRoom.id = :chatRoomId")
void bulkDeleteChatsByChatRoomId(@Param("chatRoomId") Long chatRoomId);
}
25 changes: 22 additions & 3 deletions src/main/java/com/palettee/chat/repository/ChatUserRepository.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
package com.palettee.chat.repository;

import com.palettee.chat.domain.*;
import com.palettee.chat_room.domain.ChatRoom;
import com.palettee.user.domain.User;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;

public interface ChatUserRepository extends JpaRepository<ChatUser, Long> {
Optional<ChatUser> findByChatRoomAndUser(ChatRoom chatRoom, User user);
boolean existsByChatRoomAndUser(ChatRoom chatRoom, User user);
@Query("SELECT cu from ChatUser cu " +
"WHERE cu.chatRoom.id = :chatRoomId " +
"AND cu.user.id = :userId " +
"AND cu.isDeleted = :isDeleted ")
Optional<ChatUser> findByChatRoomAndUser(@Param("chatRoomId") Long chatRoomId,
@Param("userId") Long userId,
@Param("isDeleted") boolean isDeleted);

@Query("SELECT count(cu) from ChatUser cu WHERE cu.chatRoom.id = :chatRoomId And cu.isDeleted = :isDeleted")
int countChatUsersByChatRoom(@Param("chatRoomId") Long chatRoomId,
@Param("isDeleted") boolean isDeleted);

@Modifying
@Query("delete from ChatUser cu where cu.chatRoom.id = :chatRoomId")
void deleteAllByChatRoomId(@Param("chatRoomId") Long chatRoomId);

@Query("select cu from ChatUser cu join fetch cu.user where cu.chatRoom in " +
"(select cu2.chatRoom from ChatUser cu2 where cu2.user = :user)" +
"and cu.user <> :user")
List<ChatUser> getChatUsersByMe(@Param("user") User user);
}
4 changes: 4 additions & 0 deletions src/main/java/com/palettee/chat/service/ChatImageService.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ public class ChatImageService {
public List<ChatImage> saveChatImages(List<ChatImage> chatImages) {
return chatImageRepository.saveAll(chatImages);
}

public void deleteChatImages(Long chatRoomId) {
chatImageRepository.bulkDeleteChatImagesByChatRoomId(chatRoomId);
}
}
72 changes: 37 additions & 35 deletions src/main/java/com/palettee/chat/service/ChatRedisService.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.palettee.chat.controller.dto.response.ChatCustomResponse;
import com.palettee.chat.controller.dto.response.ChatResponse;
import com.palettee.chat.repository.ChatRepository;
import com.palettee.chat_room.service.ChatRoomService;
import com.palettee.global.redis.utils.TypeConverter;
import com.palettee.user.domain.User;
import com.palettee.user.exception.UserNotFoundException;
Expand All @@ -22,26 +21,24 @@
import java.util.Set;
import java.util.stream.Collectors;

import static com.palettee.global.Const.*;

@Repository
@Slf4j
public class ChatRedisService {

private final RedisTemplate<String, ChatResponse> redisTemplate;
private final ChatRepository chatRepository;
private final UserRepository userRepository;
private final ChatRoomService chatRoomService;
private ZSetOperations<String, ChatResponse> zSetOperations;
private static final String NEW_CHAT = "NEW_CHAT";

public ChatRedisService(
@Qualifier("chatRedisTemplate") RedisTemplate<String, ChatResponse> redisTemplate,
ChatRepository chatRepository,
UserRepository userRepository,
ChatRoomService chatRoomService) {
UserRepository userRepository) {
this.redisTemplate = redisTemplate;
this.chatRepository = chatRepository;
this.userRepository = userRepository;
this.chatRoomService = chatRoomService;
}

@PostConstruct
Expand All @@ -51,80 +48,85 @@ private void init() {

public ChatResponse addChat(String email, Long chatRoomId, ChatRequest chatRequest) {
User user = getUser(email);
chatRoomService.getChatRoom(chatRoomId);

ChatResponse chatResponse = ChatResponse.toResponse(chatRoomId, user, chatRequest);
log.info("save chat sendAt = {}", chatResponse.getSendAt());
LocalDateTime sendAt = chatResponse.getSendAt();
String key = CHATROOM_KEY_PREFIX + TypeConverter.LongToString(chatResponse.getChatRoomId());
double score = TypeConverter.LocalDateTimeToDouble(chatResponse.getSendAt());

redisTemplate
.opsForZSet()
.add(TypeConverter.LongToString(chatResponse.getChatRoomId()), chatResponse, TypeConverter.LocalDateTimeToDouble(sendAt));
.add(key, chatResponse, score);

Long size = redisTemplate.opsForZSet().size(key);
if (size != null && size > CHAT_MAX_SIZE) {
redisTemplate.opsForZSet().removeRange(key, 0, 0);
}

redisTemplate
.opsForZSet()
.add(NEW_CHAT, chatResponse, TypeConverter.LocalDateTimeToDouble(sendAt));
.add(NEW_CHAT, chatResponse, score);

redisTemplate.expire(TypeConverter.LongToString(chatResponse.getChatRoomId()), Duration.ofDays(1));
redisTemplate.expire(key, Duration.ofDays(1));
return chatResponse;
}

public ChatCustomResponse getChats(Long chatRoomId, int size, LocalDateTime lastSendAt) {
String chatRoomIdStr = TypeConverter.LongToString(chatRoomId);

String key = CHATROOM_KEY_PREFIX + TypeConverter.LongToString(chatRoomId);
long offset = 0;
LocalDateTime findSendAt = lastSendAt;

if (lastSendAt == null) {
log.info("Local = {}",LocalDateTime.now());
findSendAt = LocalDateTime.now();
} else {
offset = 1;
}

Double cursor = TypeConverter.LocalDateTimeToDouble(findSendAt);
log.info("cursor = {}", cursor);
Set<ChatResponse> objects
= zSetOperations.reverseRangeByScore(chatRoomIdStr, Double.NEGATIVE_INFINITY, cursor, offset, size+1);
= zSetOperations.reverseRangeByScore(key, Double.NEGATIVE_INFINITY, cursor, offset, size+1);
List<ChatResponse> results = objects.stream().collect(Collectors.toList());

log.info("results size = {}", results.size());
redisTemplate.expire(key, Duration.ofDays(1));

// size와 작거나 같으면 DB에서 조회
if(results.size() <= size) {
// 부족한 데이터 만큼 DB에서 조회
ChatCustomResponse chatDataInDB = findOtherChatDataInDB(results, lastSendAt, chatRoomId, size - results.size());

if(!results.isEmpty()) {
results.addAll(chatDataInDB.getChats());
return ChatCustomResponse.toResponseFromDto(results, chatDataInDB.isHasNext(), chatDataInDB.getLastSendAt());
// DB에서 조회한 데이터가 존재하면 Redis에 데이터를 넣는다.
if (!chatDataInDB.getChats().isEmpty()) {
Long redisTotalSize = redisTemplate.opsForZSet().size(key);
cachingDBDataToRedis(redisTotalSize, chatDataInDB.getChats());
}

return chatDataInDB;
// DB에서 조회한 데이터 list를 Redis에서 조회한 데이터 list와 합친다.
results.addAll(chatDataInDB.getChats());
return ChatCustomResponse.toResponseFromDto(results, chatDataInDB.isHasNext());
}

LocalDateTime nextSendAt = results.get(size - 1).getSendAt();
List<ChatResponse> chats = results.subList(0, size);
return ChatCustomResponse.toResponseFromDto(chats, true, nextSendAt);
return ChatCustomResponse.toResponseFromDto(results.subList(0, size), true);
}

public ChatCustomResponse findOtherChatDataInDB(List<ChatResponse> results, LocalDateTime lastSendAt,
Long chatRoomId, int size) {
if(!results.isEmpty()) {
lastSendAt = results.get(results.size() - 1).getSendAt();
}
ChatCustomResponse chatNoOffset = chatRepository.findChatNoOffset(chatRoomId, size, lastSendAt);

if (!chatNoOffset.getChats().isEmpty()) {
cachingDBDataToRedis(chatNoOffset.getChats());
}
return chatNoOffset;
return chatRepository.findChatNoOffset(chatRoomId, size, lastSendAt);
}

public void cachingDBDataToRedis(List<ChatResponse> chatsInDB) {
for(ChatResponse chatResponse : chatsInDB) {
LocalDateTime sendAt = chatResponse.getSendAt();
// 허용 가능한 만큼만 Redis에 넣는다.
public void cachingDBDataToRedis(Long redisTotalSize, List<ChatResponse> chatsInDB) {
long possibleSize = CHAT_MAX_SIZE - redisTotalSize;
for(int i = 0; i < possibleSize; i++) {
ChatResponse chatResponse = chatsInDB.get(i);
redisTemplate
.opsForZSet()
.add(TypeConverter.LongToString(chatResponse.getChatRoomId()), chatResponse, TypeConverter.LocalDateTimeToDouble(sendAt));
.add(TypeConverter.LongToString(chatResponse.getChatRoomId()),
chatResponse, TypeConverter.LocalDateTimeToDouble(chatResponse.getSendAt()));
if(i == chatsInDB.size() - 1) {
break;
}
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/palettee/chat/service/ChatService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.palettee.chat.service;

import com.palettee.chat.repository.ChatRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ChatService {
private final ChatRepository chatRepository;

public void deleteChats(Long chatRoomId) {
chatRepository.bulkDeleteChatsByChatRoomId(chatRoomId);
}
}
34 changes: 20 additions & 14 deletions src/main/java/com/palettee/chat/service/ChatUserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,42 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ChatUserService {
private final ChatUserRepository chatUserRepository;

public void saveChatUser(ChatRoom chatRoom, User user) {
ChatUser chatUser = makeChatUser(chatRoom, user);
public void saveChatUser(ChatRoom chatRoom, User user, boolean isDeleted) {
ChatUser chatUser = makeChatUser(chatRoom, user, isDeleted);
chatUserRepository.save(chatUser);
}

public void deleteChatUser(ChatRoom chatRoom, User user) {
ChatUser chatUser = getChatUser(chatRoom, user);
chatUserRepository.delete(chatUser);
public void deleteChatUsers(Long chatRoomId) {
chatUserRepository.deleteAllByChatRoomId(chatRoomId);
}

public int countChatRoom(Long chatRoomId) {
return chatUserRepository.countChatUsersByChatRoom(chatRoomId, true);
}

public boolean isExist(ChatRoom chatRoom, User user) {
return chatUserRepository.existsByChatRoomAndUser(chatRoom, user);
public ChatUser getChatUser(Long chatRoomId, Long userId, boolean isDeleted) {
return chatUserRepository
.findByChatRoomAndUser(chatRoomId, userId, isDeleted)
.orElseThrow(() -> ChatUserNotFoundException.EXCEPTION);
}

private ChatUser makeChatUser(ChatRoom chatRoom, User user) {
public List<ChatUser> getMyChatUsers(User user) {
return chatUserRepository.getChatUsersByMe(user);
}

private ChatUser makeChatUser(ChatRoom chatRoom, User user, boolean isDeleted) {
return ChatUser.builder()
.chatRoom(chatRoom)
.user(user)
.isDeleted(isDeleted)
.build();
}

private ChatUser getChatUser(ChatRoom chatRoom, User user) {
return chatUserRepository
.findByChatRoomAndUser(chatRoom, user)
.orElseThrow(() -> ChatUserNotFoundException.EXCEPTION);
}
}
Loading

0 comments on commit 387d023

Please sign in to comment.