[Spring WebSocket & STOMP] 특정 topic에 대한 SUBSCRIBE 요청 거절하기

hyng·2022년 1월 5일
0

Spring WebSocket과 STOMP를 이용해서 채팅 웹 어플리케이션을 구현하는 중,
사용자가 제한 인원이 꽉 찬 채팅방에 SUBSCRIBE 요청을 보낼 경우 서버에서 거절 메시지를 보내서 클라이언트에서 리다이렉트 등의 처리를 할 수 있도록 하고 싶었다.
이런 식으로 특정 유저를 차단하는 기능도 구현할 수 있을 것 같다.

서버

1) ChannelInterceptorAdapter 상속, preSend 메소드 오버라이딩

먼저 ChannelInterceptorAdapter 을 상속받고 presend메소드를 오버라이딩 한다.

ChannelInterceptor 인터페이스를 구현해도 되지만 그렇게 할 경우 불필요한 메소드도 모두 구현해 주어야 하기 때문에, 편의상 ChannelInterceptorAdapter을 상속받아서 필요한 presend메소드만 오버라이딩 한다.


스프링 컨테이너에 TopicSubscriptionInterceptor을 인터셉터로 등록해 준다. 이렇게 함으로써 클라이언트로부터 오는 inbound message를 가로챌 수 있다.

2) 구현한 인터셉터 등록

package com.example.websocketdemo.config;

import com.example.websocketdemo.interceptor.TopicSubscriptionInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;


@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Autowired
    TopicSubscriptionInterceptor topicSubscriptionInterceptor;

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration){
        registration.setInterceptors(topicSubscriptionInterceptor);
    }

}

3) preSend메소드 오버라이딩

TopicSubscriptionInterceptor에 @Component 애노테이션을 붙여줘서 스프링 빈으로 등록 되도록 하고, ChatRoomRepository , MessageChannel 을 주입받는다.

@Slf4j
@Component
@RequiredArgsConstructor

public class TopicSubscriptionInterceptor extends ChannelInterceptorAdapter{

    private final ChatRoomRepository chatRoomRepository;
    private final MessageChannel clientOutboundChannel;



    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
        if(StompCommand.SUBSCRIBE.equals(accessor.getCommand())){

            MessageHeaders headers = message.getHeaders();
            MultiValueMap<String, String> map= headers.get(StompHeaderAccessor.NATIVE_HEADERS, MultiValueMap.class);
            String chatRoomId = map.getFirst("chatRoomId");

            StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.MESSAGE);
            headerAccessor.setSessionId(accessor.getSessionId());
            headerAccessor.setSubscriptionId(accessor.getSubscriptionId());
            if (!validateSubscription(chatRoomId)) {
                headerAccessor.setMessage("FULL");
                clientOutboundChannel.send(MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()));
                return null;
            }else{
                headerAccessor.setMessage("OK");
            }
            clientOutboundChannel.send(MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()));
        }
        return message;
    }

    private boolean validateSubscription(String chatRoomId) {

        ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId);
        if(chatRoom != null && chatRoom.checkCount()){
            return true;
        }
        return false;
    }
}

SUBSCRIBE 요청일 때, 현재 채팅방 인원수를 확인하여 채팅방 인원수가 최대 인원 수보다 작다면, OK 응답을 보내고 현재 채팅방 인원수가 최대 인원수와 같다면 "FULL" 을 보낸다.

예외를 던지거나 StompCommand.MESSAGE를 ERROR로 하지 않는 이유는 그렇게 할 경우 클라이언트단에서 채팅방의 인원이 꽉 찼을 때 alert 하고 리다이렉트 하는 등의 처리를 할 수 없기 때문이다.

1) 메시지에 대한 정보를 얻기 위해 메시지를 Wrapping 해 StompMessageHeaderAccessor을 얻어낸다.

2) StompHeaderAccessor.create로 메시지를 만들고, 필수 정보(sessionId, setSubscriptionId) 등을 입력한다.

참고
STOMP 서버는 모든 subscriber들에게 메시지를 브로드캐스팅 하기 위해 MESSAGE COMMAND를 사용할수 있다.

MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@

서버의 모든 메시지는 특정 클라이언트 subscription에 응답해야만 하며, 서버 메시지의 subscription-id 헤더는 클라이언트가 subscribe한 id헤더와 일치해야 한다.
출처

3) 클라이언트에서 SUBSCRIBE 요청할 때 보낸 chatRoomId를 이용해 chatRoom을 찾고 현재 인원수를 확인한 다음 setMessage() 메서드를 통해 "FULL" 또는 "OK"를 보낸다.

4) 서버가 WebSocket 클라이언트에게 메시지 보내기 위한 Channel인 clientOutboundChannel의 send메소드를 이용해 메시지를 보낸다.

클라이언트

1) 서버에 subscibe 요청 보냄

 stompClient.subscribe(`/topic/chat/${chatRoomId}`,onMessageReceived, { chatRoomId : chatRoomId });

2) 서버에서 보내는 메시지 처리

function onMessageReceived(payload) {
    if(payload.headers.message === "FULL"){

        alert('인원이 다 찼습니다!');
        window.location.href = 'http://localhost:8080/chatRoom.html';


    }else if(payload.headers.message === "OK"){
        addUser();
        return;
    }
}

결과

1) 성공

2) 실패

profile
공부하고 알게 된 내용을 기록하는 블로그

1개의 댓글

comment-user-thumbnail
2023년 5월 21일

안녕하세요! 글 너무 잘 읽었습니다! 저도 비슷한 방식으로 interceptor를 구현했는데 실행시 bean 들간의 circular dependencies가 발생했다고 오류가 뜨던데 이것은 어떤식으로 처리하셨나요??

답글 달기