Skip to content

Commit

Permalink
refactor: Redis를 사용하여 채팅 고도화하기
Browse files Browse the repository at this point in the history
Related to: Issue #6
  • Loading branch information
eckrin committed Oct 27, 2023
1 parent 619796b commit 976ccb2
Show file tree
Hide file tree
Showing 11 changed files with 192 additions and 24 deletions.
6 changes: 2 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,12 @@ dependencies {
runtimeOnly 'com.mysql:mysql-connector-j'
//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
//embedded redis - https://mvnrepository.com/artifact/it.ozimov/embedded-redis
// implementation 'it.ozimov:embedded-redis:0.7.2'
//lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'


// https://mvnrepository.com/artifact/it.ozimov/embedded-redis
compileOnly 'it.ozimov:embedded-redis:0.7.2'
}

tasks.named('test') {
Expand Down
2 changes: 0 additions & 2 deletions src/main/java/com/kusitms/jipbap/JipbapApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;

@EnableScheduling
@EnableJpaAuditing
Expand Down
23 changes: 18 additions & 5 deletions src/main/java/com/kusitms/jipbap/message/ChatController.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,38 @@
public class ChatController {

// private final ChatService chatService;
private final SimpMessageSendingOperations messagingTemplate;
// private final SimpMessageSendingOperations messagingTemplate;
private final RedisPublisher redisPublisher;
private final ChatRoomRepository chatRoomRepository;

// @PostMapping
// public ChatRoom createRoom(@RequestParam String name) {
// return chatService.createRoom(name);
// }
//

// @GetMapping
// public List<ChatRoom> findAllRoom() {
// return chatService.findAllRoom();
// }
// package com.websocket.chat.controller;

// @MessageMapping("/chat/message")
// public void message(ChatMessage message) {
// if (ChatMessage.MessageType.JOIN.equals(message.getType()))
// message.setMessage(message.getSender() + "님이 입장하셨습니다.");
// messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message); // sub/chat/room/{roomId} 경로로 메세지를 보낸다.
// }

/**
* websocket "/pub/chat/message"로 들어오는 메시징을 처리한다.
*/
@MessageMapping("/chat/message")
public void message(ChatMessage message) {
if (ChatMessage.MessageType.JOIN.equals(message.getType()))
if (ChatMessage.MessageType.ENTER.equals(message.getType())) {
chatRoomRepository.enterChatRoom(message.getRoomId());
message.setMessage(message.getSender() + "님이 입장하셨습니다.");
messagingTemplate.convertAndSend("/sub/chat/room/" + message.getRoomId(), message); // sub/chat/room/{roomId} 경로로 메세지를 보낸다.
}
// Websocket에 발행된 메시지를 redis로 발행한다(publish)
redisPublisher.publish(chatRoomRepository.getTopic(message.getRoomId()), message);
}

}
2 changes: 1 addition & 1 deletion src/main/java/com/kusitms/jipbap/message/ChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
public class ChatMessage {
// 메시지 타입 : 입장, 채팅
public enum MessageType {
JOIN, TALK
ENTER, TALK
}
private MessageType type; // 메시지 타입
private String roomId; // 방번호
Expand Down
6 changes: 5 additions & 1 deletion src/main/java/com/kusitms/jipbap/message/ChatRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@
import lombok.Setter;
import org.springframework.web.socket.WebSocketSession;

import java.io.Serial;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;

@Getter
@Setter
public class ChatRoom {
public class ChatRoom implements Serializable {

private static final long serialVersionUID = 4780475579618133057L;
private String roomId;
private String name;

Expand Down
57 changes: 47 additions & 10 deletions src/main/java/com/kusitms/jipbap/message/ChatRoomRepository.java
Original file line number Diff line number Diff line change
@@ -1,34 +1,71 @@
package com.kusitms.jipbap.message;

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Repository;

import java.util.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* HGET CHAT_ROOM(key) aeif-4ifehw-2ef-4g5(room id) value(chatroom)
* redis hash의
*/
@RequiredArgsConstructor
@Repository
public class ChatRoomRepository {

private Map<String, ChatRoom> chatRoomMap;
// 채팅방(topic)에 발행되는 메시지를 처리할 Listener
private final RedisMessageListenerContainer redisMessageListener;
// 구독 처리 서비스
private final RedisSubscriber redisSubscriber;
// Redis
private static final String CHAT_ROOMS = "CHAT_ROOM";
private final RedisTemplate<String, Object> redisTemplate;
private HashOperations<String, String, ChatRoom> opsHashChatRoom;
// 채팅방의 대화 메시지를 발행하기 위한 redis topic 정보. 서버별로 채팅방에 매치되는 topic정보를 Map에 넣어 roomId로 찾을수 있도록 한다.
private Map<String, ChannelTopic> topics;

@PostConstruct
private void init() {
chatRoomMap = new LinkedHashMap<>();
opsHashChatRoom = redisTemplate.opsForHash();
topics = new HashMap<>();
}

public List<ChatRoom> findAllRoom() {
// 채팅방 생성순서 최근 순으로 반환
List chatRooms = new ArrayList<>(chatRoomMap.values());
Collections.reverse(chatRooms);
return chatRooms;
return opsHashChatRoom.values(CHAT_ROOMS);
}

public ChatRoom findRoomById(String id) {
return chatRoomMap.get(id);
return opsHashChatRoom.get(CHAT_ROOMS, id);
}

/**
* 채팅방 생성 : 서버간 채팅방 공유를 위해 redis hash에 저장한다.
*/
public ChatRoom createChatRoom(String name) {
ChatRoom chatRoom = ChatRoom.create(name);
chatRoomMap.put(chatRoom.getRoomId(), chatRoom);
opsHashChatRoom.put(CHAT_ROOMS, chatRoom.getRoomId(), chatRoom);
return chatRoom;
}

/**
* 채팅방 입장 : redis에 topic을 만들고 pub/sub 통신을 하기 위해 리스너를 설정한다.
*/
public void enterChatRoom(String roomId) {
ChannelTopic topic = topics.get(roomId);
if (topic == null) {
topic = new ChannelTopic(roomId);
redisMessageListener.addMessageListener(redisSubscriber, topic);
topics.put(roomId, topic);
}
}

public ChannelTopic getTopic(String roomId) {
return topics.get(roomId);
}
}
30 changes: 30 additions & 0 deletions src/main/java/com/kusitms/jipbap/message/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.kusitms.jipbap.message;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

//redis pub/sub을 처리하는 Listener
@Bean
public RedisMessageListenerContainer redisMessageListener(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/kusitms/jipbap/message/RedisPublisher.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.kusitms.jipbap.message;

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.stereotype.Service;

/**
* 메세지를 redis topic에 publish해주는 서비스
*/
@RequiredArgsConstructor
@Service
public class RedisPublisher {
private final RedisTemplate<String, Object> redisTemplate;

public void publish(ChannelTopic topic, ChatMessage message) {
redisTemplate.convertAndSend(topic.getTopic(), message);
}
}
38 changes: 38 additions & 0 deletions src/main/java/com/kusitms/jipbap/message/RedisSubscriber.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.kusitms.jipbap.message;

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {

private final ObjectMapper objectMapper;
private final RedisTemplate redisTemplate;
private final SimpMessageSendingOperations messagingTemplate;

/**
* RedisPublisher에서 pub된 메세지를 대기하고 있던 onMessage가 받아 처리한다.
* messagingTemplate을 사용하여 subscriber에게 메세지를 전달한다.
*/
@Override
public void onMessage(Message message, byte[] pattern) {
try {
// redis에서 발행된 데이터를 받아 deserialize
String publishMessage = (String) redisTemplate.getStringSerializer().deserialize(message.getBody());
// ChatMessage 객채로 맵핑
ChatMessage roomMessage = objectMapper.readValue(publishMessage, ChatMessage.class);
// Websocket 구독자에게 채팅 메시지 Send
messagingTemplate.convertAndSend("/sub/chat/room/" + roomMessage.getRoomId(), roomMessage);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ public void configureMessageBroker(MessageBrokerRegistry config) {
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-stomp") //websocket 연결 endpoint
.setAllowedOriginPatterns("*");
// .withSockJS(); //제거시 endpoint 연결 성공
// .withSockJS(); //제거해야 endpoint 연결 성공
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//package com.kusitms.jipbap.message;
//
//import jakarta.annotation.PostConstruct;
//import jakarta.annotation.PreDestroy;
//import org.springframework.beans.factory.annotation.Value;
//import org.springframework.context.annotation.Configuration;
//import redis.embedded.RedisServer;
//
//@Configuration
//public class EmbeddedRedisConfig {
//
// @Value("${spring.data.redis.port}")
// private int redisPort;
//
// private RedisServer redisServer;
//
// @PostConstruct
// public void redisServer() {
// redisServer = RedisServer.builder()
// .port(6380)
// .build();
// redisServer.start(); //doesn't work in macos 14 sonoma
// }
//
// @PreDestroy
// public void stopRedis() {
// if (redisServer != null) {
// redisServer.stop();
// }
// }
//}

0 comments on commit 976ccb2

Please sign in to comment.