// redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
// embedded-redis
compileOnly group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
spring.profiles.active=local
spring.data.redis.port=6379
spring.data.redis.host=localhost
spring:
profiles:
active: alpha
redis:
host: redis가 설치된 서버 호스트
port: redis가 설치된 서버 포트
@PostConstruct, @PreDestroy - 채팅 서버가 실행될 때 Embedded Redis 서버도 동시에 실행될 수 있도록
@Profile(”local”) - local 환경에서만 실행되도록
@Profile("local")
@Configuration
public class EmbeddedRedisConfig {
@Value("${spring.redis.port}")
private int redisPort;
private RedisServer redisServer;
@PostConstruct
public void redisServer() {
redisServer = new RedisServer(redisPort);
redisServer.start();
}
@PreDestroy
public void stopRedis() {
if(redisServer != null) {
redisServer.stop();
}
}
}
MessageListener 추가 - Redis의 pub/sub 이용
RedisTemplate 설정 - 어플리케이션에서 redis 사용
@Configuration
public class RedisConfig {
/*
redis pub/sub 메세지를 처리하는 listener 설정
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
/*
어플리케이션에서 사용할 redisTemplate 설정
*/
@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;
}
}
채팅방에 입장 후 메세지 작성
→ 메세지를 Redis Topic에 발행
@RequiredArgsConstructor
@Service
public class RedisPublisher {
private final RedisTemplate<String, Object> redisTemplate;
/*
채팅방에 입장 후 메세지 작성
-> 해당 메세지 Redis Topic에 발행
-> 대기하고 있던 redis 구독 서비스가 메세지 처리
*/
public void publish(ChannelTopic topic, ChatMessage message) {
redisTemplate.convertAndSend(topic.getTopic(), message);
}
}
Redis에 메세지 발행이 될 때까지 대기
발행되면 해당 메세지 읽어 처리
- Redis 발행 메세지 → ChatMessage 변환 → messagingTemplate으로 채팅방의 모든 클라이언트에게 메세지 전달
@Slf4j(topic = "RedisSubscriber")
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {
private final ObjectMapper objectMapper;
private final RedisTemplate redisTemplate;
private final SimpMessageSendingOperations messagingTemplate;
/*
Redis에서 메세지가 발행(publish)
-> 대기하고있던 onMessage() 가 해당 메세지 처리
*/
@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 구독자에게 ChatMessage 발송
messagingTemplate.convertAndSend("/sub/chat/room/" + roomMessage.getRoomId(), roomMessage);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
@Configuration
public class RedisConfig {
@Value("${redis.host}")
private String redisHost;
@Value("${redis.port}")
private int redisPort;
// Redis 저장소와 연결
@Bean
public RedisConnectionFactory connectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}
/*
redis pub/sub 메세지를 처리하는 listener 설정
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer() {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory());
return container;
}
/*
어플리케이션에서 사용할 redisTemplate 설정
*/
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory());
// RedisTemplate을 사용할 때 Spring-Redis 간 데이터 직렬화/역직렬화 시 사용하는 방식이 jdk 직렬화 방식
// 동작에는 문제가 없지만 redis-cli를 통해 데이터를 확인할 때 알아볼 수 없는 형태로 출력되기 때문
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}
}
enterChatRoom() - 클라이언트 입장 시 채팅방(topic)에서 대화 가능하도록 리스너 연동
- 채팅방에 발행된 메세지 → 서로 다른 서버에 공유하기 위해 redis의 topic으로 발행
@RequiredArgsConstructor
@Controller
@Slf4j(topic = "ChatController")
public class ChatController {
private final RedisPublisher redisPublisher;
private final ChatRoomRepository chatRoomRepository;
/*
Websocket "/pub/chat/message"로 들어오는 메세지 처리
*/
@MessageMapping("/chat/message")
public void message(ChatMessage message) {
// 입장 메세지일 경우
if (ChatMessage.MessageType.ENTER.equals(message.getType())) {
chatRoomRepository.enterChatRoom(message.getRoomId());
message.setMessage(message.getSender() + "님이 입장하셨습니다.");
}
// Websocket 에 발행된 메세지 redis 로 발행 (publish)
redisPublisher.publish(chatRoomRepository.getTopic(message.getRoomId()), message);
}
}
채팅방 정보가 초기화되지 않도록 생성시 Redis Hash에 저장
@RequiredArgsConstructor
@Repository
public class ChatRoomRepository {
// 채팅방 (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() {
opsHashChatRoom = redisTemplate.opsForHash();
topics = new HashMap<>();
}
public List<ChatRoom> findAllRoom() {
return opsHashChatRoom.values(CHAT_ROOMS);
}
public ChatRoom findRoomById(String id) {
return opsHashChatRoom.get(CHAT_ROOMS, id);
}
/*
채팅방 생성 : 서버간 채팅방 공유를 위해 redis hash에 저장
*/
public ChatRoom createChatRoom(String name) {
ChatRoom chatRoom = ChatRoom.create(name);
opsHashChatRoom.put(CHAT_ROOMS, chatRoom.getRoomId(), chatRoom);
return chatRoom;
}
/*
채팅방 입장 : redis 에 topic 을 만들고 pub/sub 통신을 하기 위해 listener 설정
*/
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);
}
}
Serializable
- Redis에 저장되는 객체들은 Serialize 가능해야함
- serialVersionUID
@Getter
public class ChatRoom implements Serializable {
@Serial
private static final long serialVersionUID = 6494678977089006639L;
private String roomId;
private String name;
public static ChatRoom create(String name) {
ChatRoom chatRoom = new ChatRoom();
chatRoom.roomId = UUID.randomUUID().toString();
chatRoom.name = name;
return chatRoom;
}
}
**gradlew.bat build**
**cd build/libs**
**java -jar -Dserver.port=8090 websocket-prac-0.0.1-SNAPSHOT.jar**
**gradlew.bat clean**