최대한 다른 기능들을 제외하고 단순한 채팅 프로그램구현으로
전체적인 뼈대를 구성해본다.
@Configuraion
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configurMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setAppliactionDestinationPrefixes("/app");
}
@Override
public void registerStompEndPoints(StompEndPointRegistry registry) {
registry.addEndPoint("/ws")
.setAllowedOrigins("http://127.0.0.1:5500")
.withSockJs();
}
}
메세지 전달 위주의 웹소켓 사용으로 @EnableWebSocketMessageBroker 어노테이션을 사용해서 SimpMessagingTemplate가 등록되어 사용할 수 있다.
configureMessageBroker
RegisterStompEndPoints
@Getter
public class MessageDto {
private String sender;
private String content;
public MessageDto() {}
}
//-----------------------------------------------------------------------------------------------
private final SimpMessagingTemplate simpMessagingTemplate;
private final RedisTemplate<String, Object> redisTemplate;
@MessageMapping("/{roomId}")
public void takeAndSendMessage(
@DestinationVariable Long roomId,
@PayLoad MessageDto message
) {
String key = "roomId" + roomId;
long ago = Duration.ofDays(1).toMillis();
redisTemplate.opsForZset().add(key, message, ago);
simpMessagingTemplate.convertAndSend("/topic/" + roomId, message);
}
레디스가 MessageDto를 직렬화 하기 위해서 getter, 기본 생성자가 필요하다.
메세지의 저장 기한 + 시간순으로 정렬 을 위해 Zset 자료구조를 사용한다.
opsForZset().add(key, value, score)
simpMessagingTemplate.convertAndSend(sub, payLoad);
sub의 경로를 구독한 클라이언트들에게 payload 전달
@ConfigurationProperties(prefix = "spring.redis")
@Setter
@Getter
public class RedisProperties {
private String host;
private int port;
private String password;
}
@Configuration
public class RedisConfig {
private final RedisProperties redisProperties;
public RedisConfig(RedisProperties redisProperties) {
this.redisProperties = redisProperties;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandAloneConfiguration configuration = new RedisStandAloneConfiguration();
configuration.setHost(redisProperties.getHost());
configuration.setPort(redisProperties.getPort());
configuration.setPassword(redisProperties.getPassword());
return new LettusConnectionFactory(configuration);
}
@Bean
public class RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(connectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
}
레디스의 연결설정을 위해 RedisConnectionFactory를 설정한다.
단일 레디스로 설정하기 때문에 RedisStandAloneConfiguration로 설정하여
host, port, password등을 기입한다.
이후 설정한 connectionFactory를 활용하기 위한 Template도 key, value의 직렬화 방법을 직접 선언한다.
key의 경우 문자열로 직렬화 하는 방법을 선택하고 value는 GenericJackson2JsonRedisSerializer로 선택한다.
value를 json형식으로 직렬화 시키지 않는다면 오류가 발생한다.
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatController {
private final RedisTemplate<String, Object> redisTemplate;
private final SimpMessagingTemplate messagingTemplate;
// In-memory WebSocket session management
private final Map<String, List<WebSocketSession>> chatRooms = new ConcurrentHashMap<>();
@MessageMapping("/sendMessage/{roomId}")
public void sendMessage(@DestinationVariable String roomId, MessageDto message) {
String redisKey = "chatroom:" + roomId;
// Redis에 메시지 저장 (24시간 TTL 설정)
redisTemplate.opsForList().rightPush(redisKey, message);
redisTemplate.expire(redisKey, Duration.ofHours(24));
// WebSocket으로 메시지 즉시 전달
messagingTemplate.convertAndSend("/topic/" + roomId, message);
}
@GetMapping("/messages/{roomId}")
public List<Object> getMessages(@PathVariable String roomId) {
String redisKey = "chatroom:" + roomId;
// Redis에서 메시지 조회
return redisTemplate.opsForList().range(redisKey, 0, -1);
}
}
public class ChatDto {
@Getter
@Setter
public static class MessageDto {
private String sender;
private String content;
private String timestamp;
public MessageDto(String sender, String content, String timestamp) {
this.sender = sender;
this.content = content;
this.timestamp = timestamp;
}
}
}
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedwisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName("localhost");
config.setPort(6379);
config.setPassword("qwer1234");
return new LettuceConnectionFactory(config);
}
}
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic"); //클라이언트 입장에서 해당 주소로 구독
registry.setApplicationDestinationPrefixes("/app"); //클라이언트 입장에서 해당 주소로 메세지 전송
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws") //클라이언트에서는 해당 주소로 웹소켓 연결 시도
.setAllowedOriginPatterns("http://172.25.224.1:5500") // 서버에서 허용하는 클라이언트 주소
.withSockJS(); //ws를 사용하지 않는 클라이언트는 sockjs 지원함
}
}
@Configuration
public class WebCorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedMethods("POST", "GET")
.allowedOrigins("http://172.25.224.1:5500")
.allowCredentials(true);
}
}