diff --git a/src/main/java/com/_data/_data/aichat/repository/ChatRoomRepository.java b/src/main/java/com/_data/_data/aichat/repository/ChatRoomRepository.java index 22841e3..51f4c48 100644 --- a/src/main/java/com/_data/_data/aichat/repository/ChatRoomRepository.java +++ b/src/main/java/com/_data/_data/aichat/repository/ChatRoomRepository.java @@ -2,7 +2,11 @@ import com._data._data.aichat.entity.ChatRoom; 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; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Optional; @@ -19,4 +23,16 @@ public interface ChatRoomRepository extends JpaRepository { List findByUserIdOrderByUpdatedAtDesc(Long userId); void deleteChatRoomByUserId(Long userId); + + void deleteChatRoomById(Long id); + + // 벌크 삭제 메서드 - 성능 최적화 + @Transactional + @Modifying + @Query("DELETE FROM ChatRoom c WHERE c.id IN :ids") + int deleteAllByIdIn(@Param("ids") List ids); + + @Query("SELECT cr From ChatRoom cr WHERE cr.userId = :userId AND " + + "(SELECT COUNT(m) FROM Message m WHERE m.chatRoomId = cr.id) = :messageCount") + List findByUserIdAndMessageCount(@Param("userId") Long userId, @Param("messageCount") int messageCount); } diff --git a/src/main/java/com/_data/_data/aichat/repository/MessageRepository.java b/src/main/java/com/_data/_data/aichat/repository/MessageRepository.java index ba0c0d9..b29d229 100644 --- a/src/main/java/com/_data/_data/aichat/repository/MessageRepository.java +++ b/src/main/java/com/_data/_data/aichat/repository/MessageRepository.java @@ -2,7 +2,11 @@ import com._data._data.aichat.entity.Message; 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; import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; import java.util.Comparator; import java.util.List; @@ -21,4 +25,15 @@ default List findRecent10ByChatRoomIdInChronologicalOrder(Long chatRoom recentMessages.sort(Comparator.comparing(Message::getStoredAt)); return recentMessages; } + + int countByChatRoomId(Long chatRoomId); + + @Query("SELECT m.chatRoomId, COUNT(m) FROM Message m WHERE m.chatRoomId IN :chatRoomIds GROUP BY m.chatRoomId") + List countMessagesByChatRoomIds(@Param("chatRoomIds") List chatRoomIds); + + // 벌크 삭제 메서드 - 성능 최적화 + @Transactional + @Modifying + @Query("DELETE FROM Message m WHERE m.chatRoomId IN :chatRoomIds") + int deleteAllByChatRoomIdIn(@Param("chatRoomIds") List chatRoomIds); } diff --git a/src/main/java/com/_data/_data/aichat/service/ChatRoomServiceImpl.java b/src/main/java/com/_data/_data/aichat/service/ChatRoomServiceImpl.java index b295ad3..d148e98 100644 --- a/src/main/java/com/_data/_data/aichat/service/ChatRoomServiceImpl.java +++ b/src/main/java/com/_data/_data/aichat/service/ChatRoomServiceImpl.java @@ -4,18 +4,28 @@ import com._data._data.aichat.entity.Topic; import com._data._data.aichat.exception.TopicNotFoundException; import com._data._data.aichat.repository.ChatRoomRepository; +import com._data._data.aichat.repository.MessageRepository; import com._data._data.aichat.repository.TopicRepository; import com._data._data.auth.entity.CustomUserDetails; -import com._data._data.game.repository.UserGameInfoRepository; import com._data._data.user.entity.Users; import com._data._data.user.repository.UserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor public class ChatRoomServiceImpl implements ChatRoomService { @@ -23,7 +33,7 @@ public class ChatRoomServiceImpl implements ChatRoomService { private final TopicRepository topicRepository; private final ChatRoomRepository chatRoomRepository; private final UserRepository userRepository; - private final UserGameInfoRepository userGameInfoRepository; + private final MessageRepository messageRepository; @Override public ChatRoom creatChatRoom(Long topicId) { @@ -53,7 +63,110 @@ public List getChatRooms() { CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal(); Users user = userRepository.findByEmailAndIsDeletedFalse(userDetails.getUsername()); - return chatRoomRepository.findByUserIdOrderByUpdatedAtDesc(user.getId()); + List allChatRooms = chatRoomRepository.findByUserIdOrderByUpdatedAtDesc(user.getId()); + + if (allChatRooms.isEmpty()) { + return allChatRooms; + } + + List chatRoomIds = allChatRooms.stream() + .map(ChatRoom::getId) + .toList(); + + Map messageCountMap = getMessageCountMap(chatRoomIds); + + List filteredChatRooms = allChatRooms.stream() + .filter(chatRoom -> { + Integer messageCount = messageCountMap.getOrDefault(chatRoom.getId(), 0); + + return messageCount >= 2; + }) + .toList(); + + List toDeleteChatRoomIds = allChatRooms.stream() + .filter(chatRoom -> { + Integer messageCount = messageCountMap.getOrDefault(chatRoom.getId(), 0); + + return messageCount == 1; + }) + .map(ChatRoom::getId) + .toList(); + + if (!toDeleteChatRoomIds.isEmpty()) { + log.info("비동기 삭제 대상 채팅방 개수: {}", toDeleteChatRoomIds.size()); + deleteChatRoomsAsync(toDeleteChatRoomIds); + } + + return filteredChatRooms; + } + + private Map getMessageCountMap(List chatRoomIds) { + if (chatRoomIds == null || chatRoomIds.isEmpty()) { + return new HashMap<>(); + } + + try { + List results = messageRepository.countMessagesByChatRoomIds(chatRoomIds); + + return results.stream() + .filter(Objects::nonNull) // null 체크 추가 + .collect(Collectors.toMap( + result -> (Long) result[0], + result -> ((Number) result[1]).intValue(), // 안전한 형변환 + (existing, replacement) -> existing // 중복 키 처리 + )); + } catch (Exception e) { + log.error("메시지 개수 조회 중 오류 발생", e); + return new HashMap<>(); + } + } + + @Async("chatRoomTaskExecutor") + @Transactional(propagation = Propagation.REQUIRES_NEW) + public CompletableFuture deleteChatRoomsAsync(List chatRoomIds) { + log.info("비동기 삭제 시작 - 채팅방 ID: {}", chatRoomIds); + + try { + int batchSize = 50; + + for (int i = 0; i < chatRoomIds.size(); i += batchSize) { + int endIndex = Math.min(i + batchSize, chatRoomIds.size()); + List batchIds = chatRoomIds.subList(i, endIndex); + + deleteChatRoomBatch(batchIds); + + if (endIndex < chatRoomIds.size()) { + Thread.sleep(50); + } + } + + log.info("비동기 삭제 완료 - 총 {}개 채팅방 삭제", chatRoomIds.size()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("비동기 삭제 중 인터럽트 발생", e); + throw new RuntimeException("채팅방 삭제가 중단되었습니다", e); + } catch (Exception e) { + log.error("비동기 삭제 중 오류 발생", e); + throw new RuntimeException("채팅방 삭제 중 오류가 발생했습니다", e); + } + + return CompletableFuture.completedFuture(null); } + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void deleteChatRoomBatch(List chatRoomIds) { + try { + // 1. 벌크 삭제로 성능 향상 + int deletedMessages = messageRepository.deleteAllByChatRoomIdIn(chatRoomIds); + log.debug("삭제된 메시지 수: {}", deletedMessages); + + int deletedChatRooms = chatRoomRepository.deleteAllByIdIn(chatRoomIds); + log.debug("삭제된 채팅방 수: {}", deletedChatRooms); + + log.debug("배치 삭제 완료 - 채팅방 ID: {}", chatRoomIds); + } catch (Exception e) { + log.error("배치 삭제 중 오류 발생 - 채팅방 ID: {}", chatRoomIds, e); + throw e; + } + } } diff --git a/src/main/java/com/_data/_data/common/config/AsyncConfig.java b/src/main/java/com/_data/_data/common/config/AsyncConfig.java new file mode 100644 index 0000000..33c47a9 --- /dev/null +++ b/src/main/java/com/_data/_data/common/config/AsyncConfig.java @@ -0,0 +1,26 @@ +package com._data._data.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean(name = "chatRoomTaskExecutor") + public Executor chatRoomTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(5); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("ChatRoom-Async-"); + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + executor.initialize(); + return executor; + } +}