사실 이전에도 몇 번 포스팅을 작성한 적이 있어서 간단하게만 적고 넘어가야겠다.
stomp는 웹소켓 위에서 동작하는 프로토콜로, SUB-PUB 구조를 기반으로 동작한다.
stomp를 사용하게 되면 Spring boot Websocket 앱은 클라이언트에 대한 STOMP 브로커의 역할을 하게 된다.
@Controller
메시지 핸들링 메소드로 전달되거나 simple in-memory broker로 전달된다.또한 전용 STOMP 브로커(RabbitMQ, ActiveMQ..)와 같이 동작하게 앱을 구성할 수도 있다.
STOMP를 사용할 때의 장점은 다음과 같이 있다.
WebSocketHandler
를 직접 구성하지 않고 @Controller
로 로직을 관리할 수 있음build.gradle
에 웹소켓을 사용을 위해 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-websocket'
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry endpointRegistry) {
endpointRegistry.addEndpoint("/ws")
.setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry brokerRegistry) {
brokerRegistry.enableSimpleBroker("/sub");
brokerRegistry.setApplicationDestinationPrefixes("/pub");
}
}
@EnableWebSocketMessageBroker
: STOMP를 사용할 수 있게 해주는 어노테이션registerStompEndpoints()
에서 웹소켓 연결을 위해 사용할 엔드포인트 등록과 cors 오류 방지를 위해 허용할 Origin을 등록해둔다.*
는 전체 오리진 허용이니 특정 url만 등록하도록하자 ...configureMessageBroker
에서 구독과 발행 시 사용할 prefix를 정해준다.enableSimpleBroker
는 /sub
가 prefix인 destination을 가진 메시지를 브로커로 라우팅해주는 설정이다.setApplicationDestinationPrefixes
는 /pub
가 prefix로 붙은 메시지를 @Controller
내에서 @MessageMapping
이 붙은 메소드로 라우팅하겠다는 설정이다.Redis는 Pub/Sub를 지원하기 때문에 STOMP의 메시지 브로커처럼 사용할 수 있다.
완전한 메시지 브로커 기능을 원하면 RabbitMQ 같은 걸 붙여야 한다...
@Slf4j
@Configuration
public class RedisConfig {
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(
RedisConnectionFactory redisConnectionFactory,
MessageListenerAdapter messageListenerAdapter,
ChannelTopic channelTopic
) {
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
redisMessageListenerContainer.addMessageListener(messageListenerAdapter, channelTopic);
return redisMessageListenerContainer;
}
@Bean
public MessageListenerAdapter messageListenerAdapter(RedisSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "onMessage");
}
@Bean
public ChannelTopic channelTopic() {
return new ChannelTopic("/sub/chat");
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new Jackson2JsonRedisSerializer<>(String.class));
return redisTemplate;
}
}
channelTopic
만 감시하다가 subscriber로 넘겨주기 때문에, 클라이언트는 /sub/chat
으로만 메시지를 발행하면 서버가 subscriber에서 확인 후 올바른 채널로 다시 옮겨주게끔 구조를 작성했다.@Slf4j
@RequiredArgsConstructor
@Service
public class RedisSubscriber implements MessageListener {
private final RedisTemplate<String, Object> redisTemplate;
private final ObjectMapper objectMapper;
private final SimpMessageSendingOperations sendingOperations;
@Override
public void onMessage(Message message, byte[] pattern) {
try {
String publishMessage = redisTemplate.getStringSerializer().deserialize(message.getBody());
ChatPublishDto chatPublishDto = objectMapper.readValue(publishMessage, ChatPublishDto.class);
sendingOperations.convertAndSend("/sub/chat/1", chatPublishDto);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
}
ChatPublishDto
로 대략 만들어주고 위에 등록해둔 channelTopic
인 /sub/chat
으로 메시지가 들어오면, 여기서 SimpMessageSendingOperation
을 활용해 각 유저들의 진짜 목적지로 보내주게 된다.@Slf4j
@RequiredArgsConstructor
@RestController
public class WebSocketController {
private final RedisTemplate<String, Object> redisTemplate;
private final WebSocketService webSocketService;
/** 소켓을 통해 메시지가 들어오면 받아서 해당되는 채널로 전달 */
@MessageMapping("/chat")
public void receiveAndSendMessage(ChatPublishDto chatPublishDto, SimpMessageHeaderAccessor headerAccessor) {
log.info("SEND_CHAT_SUCCESS (201 CREATED) ::");
redisTemplate.convertAndSend("/sub/chat", chatPublishDto);
}
MessageMapping
어노테이션이 붙은 controller를 통해 소켓 메시지가 전달된다redisTemplate.convertAndSend
를 통해 메시지를 보내면 위에서 작성한 subscriber
로 전달된다