[Spring boot] 웹소켓 세팅 + redis Pub/Sub 함께 이용하기

찐찐·2023년 7월 19일
0

STOMP를 사용하기 위한 세팅

0. STOMP는 무엇이고 왜 사용하는가

사실 이전에도 몇 번 포스팅을 작성한 적이 있어서 간단하게만 적고 넘어가야겠다.

  • stomp는 웹소켓 위에서 동작하는 프로토콜로, SUB-PUB 구조를 기반으로 동작한다.

  • stomp를 사용하게 되면 Spring boot Websocket 앱은 클라이언트에 대한 STOMP 브로커의 역할을 하게 된다.

    • 메시지는 @Controller 메시지 핸들링 메소드로 전달되거나 simple in-memory broker로 전달된다.
  • 또한 전용 STOMP 브로커(RabbitMQ, ActiveMQ..)와 같이 동작하게 앱을 구성할 수도 있다.

    • 이렇게 되면, Spring 앱은 브로커에 대한 TCP 연결을 관리하고, 브로커에게 메시지를 전달하고, 연결된 웹소켓 클라이언트에게 메시지를 전달하는 역할을 한다.
  • STOMP를 사용할 때의 장점은 다음과 같이 있다.

    1. 메시지 브로커를 사용해 메시지를 관리할 수 있음
    2. WebSocketHandler 를 직접 구성하지 않고 @Controller로 로직을 관리할 수 있음

1. 의존성 추가

build.gradle에 웹소켓을 사용을 위해 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-websocket'

2. WebSocketConfig 작성

@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 이 붙은 메소드로 라우팅하겠다는 설정이다.

3. Redis를 메시지 브로커로 사용하기

Redis는 Pub/Sub를 지원하기 때문에 STOMP의 메시지 브로커처럼 사용할 수 있다.
완전한 메시지 브로커 기능을 원하면 RabbitMQ 같은 걸 붙여야 한다...

3-1. RedisConfig 작성

@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에서 확인 후 올바른 채널로 다시 옮겨주게끔 구조를 작성했다.

3-2. RedisSubscriber 작성

@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을 활용해 각 유저들의 진짜 목적지로 보내주게 된다.

4. WebSocketController 구현

@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로 전달된다

참고

profile
백엔드 개발자 지망생

0개의 댓글