Skip to content

Commit

Permalink
feat: WebSocket, STOMP, Redis pub/sub을 활용한 채팅 기능 구현(예외 제외) #81
Browse files Browse the repository at this point in the history
  • Loading branch information
yhpark95 committed Sep 21, 2023
1 parent 26e6e69 commit 9d0e109
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import kr.codesquad.chat.dto.request.ChatRoomCreateRequest;
import kr.codesquad.chat.dto.request.SendMessageRequest;
import kr.codesquad.chat.dto.response.ChatRoomCreateResponse;
import kr.codesquad.chat.service.ChatService;
import kr.codesquad.util.Constants;
Expand All @@ -29,4 +31,8 @@ public ResponseEntity<ChatRoomCreateResponse> createChatRoom(
.body(chatService.createRoom(chatRoomCreateRequest, loginId));
}

@MessageMapping("/comm/message")
public void message(SendMessageRequest message) {
chatService.sendMessage(message);
}
}
4 changes: 4 additions & 0 deletions be/src/main/java/kr/codesquad/chat/dto/ChatMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
import org.mapstruct.factory.Mappers;

import kr.codesquad.chat.dto.request.ChatRoomCreateRequest;
import kr.codesquad.chat.dto.request.SendMessageRequest;
import kr.codesquad.chat.dto.response.ChatRoomCreateResponse;
import kr.codesquad.chat.entity.ChatMessage;
import kr.codesquad.chat.entity.ChatRoom;

@Mapper
Expand All @@ -17,4 +19,6 @@ public interface ChatMapper {

@Mapping(target = "chatRoomId", source = "id")
ChatRoomCreateResponse toChatRoomCreateResponse(ChatRoom chatRoom);

ChatMessage toChatMessage(SendMessageRequest sendMessageRequest);
}
37 changes: 13 additions & 24 deletions be/src/main/java/kr/codesquad/chat/service/ChatService.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
package kr.codesquad.chat.service;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.transaction.annotation.Transactional;

import com.fasterxml.jackson.databind.ObjectMapper;

import kr.codesquad.chat.dto.ChatMapper;
import kr.codesquad.chat.dto.request.ChatRoomCreateRequest;
import kr.codesquad.chat.dto.request.SendMessageRequest;
import kr.codesquad.chat.dto.response.ChatRoomCreateResponse;
import kr.codesquad.chat.entity.ChatMessage;
import kr.codesquad.chat.repository.ChatMessageRepository;
import kr.codesquad.chat.repository.ChatRoomRepository;
import kr.codesquad.user.entity.User;
Expand All @@ -32,30 +28,23 @@ public class ChatService {
private final ChatMessageRepository chatMessageRepository;
private final UserRepository userRepository;
private final ObjectMapper objectMapper;
private final ConcurrentMap<Long, Set<WebSocketSession>> sessionMap = new ConcurrentHashMap<>();
private final ChannelTopic channelTopic;
private final RedisTemplate redisTemplate;

public ChatRoomCreateResponse createRoom(ChatRoomCreateRequest chatRoomCreateRequest, String loginId) {
User user = userRepository.findByLoginId(loginId);
return ChatMapper.INSTANCE.toChatRoomCreateResponse(
chatRoomRepository.save(ChatMapper.INSTANCE.toChatRoom(chatRoomCreateRequest, user.getId())));
}

public void sendMessage(Long chatRoomId, WebSocketSession session, SendMessageRequest message) {
if (!sessionMap.containsKey(chatRoomId)) {
sessionMap.put(chatRoomId, new HashSet<>());
}
Set<WebSocketSession> sessions = sessionMap.get(chatRoomId);
sessions.add(session);
sessionMap.put(chatRoomId, sessions);
sessions.parallelStream().forEach(s -> sendMessage(s, message));
}
@Transactional
public void sendMessage(SendMessageRequest chatMessageRequest) {

//채팅 생성 및 저장
ChatMessage chatMessage = chatMessageRepository.save(ChatMapper.INSTANCE.toChatMessage(chatMessageRequest));
String topic = channelTopic.getTopic();

private <T> void sendMessage(WebSocketSession session, T message) {
try {
session.sendMessage(new TextMessage(objectMapper.writeValueAsString(message)));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
redisTemplate.convertAndSend(topic, chatMessageRequest);
}

}
36 changes: 36 additions & 0 deletions be/src/main/java/kr/codesquad/chat/service/RedisSubscriber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package kr.codesquad.chat.service;

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.databind.ObjectMapper;

import kr.codesquad.chat.dto.request.SendMessageRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {
private final ObjectMapper objectMapper;
private final RedisTemplate redisTemplate;
private final SimpMessageSendingOperations messagingTemplate;

@Override
public void onMessage(Message message, byte[] pattern) {
try {
String publishMessage = (String)redisTemplate.getStringSerializer().deserialize(message.getBody());

SendMessageRequest roomMessage = objectMapper.readValue(publishMessage, SendMessageRequest.class);

messagingTemplate.convertAndSend("/sub/chat/room/" + roomMessage.getChatRoomId(), roomMessage.getContent());

} catch (Exception e) {
throw new RuntimeException("뭐");
}
}
}
30 changes: 28 additions & 2 deletions be/src/main/java/kr/codesquad/core/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import kr.codesquad.chat.service.RedisSubscriber;
import kr.codesquad.redis.RedisProperties;
import lombok.RequiredArgsConstructor;

Expand All @@ -28,8 +33,29 @@ public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());

redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}

@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter,
ChannelTopic channelTopic
) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.addMessageListener(listenerAdapter, channelTopic);
return container;
}

@Bean
public MessageListenerAdapter listenerAdapter(RedisSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "onMessage");
}

@Bean
public ChannelTopic channelTopic() { // (4)
return new ChannelTopic("chatRoom");
}
}
24 changes: 15 additions & 9 deletions be/src/main/java/kr/codesquad/core/config/WebSocketConfig.java
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
package kr.codesquad.core.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

import kr.codesquad.core.websocket.WebSocketHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
@RequiredArgsConstructor
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws/chat").setAllowedOrigins("*");
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws/chat")
.setAllowedOriginPatterns("*");
}

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/sub");
registry.setApplicationDestinationPrefixes("/pub");
}
}

0 comments on commit 9d0e109

Please sign in to comment.