spring

(Spring) Stomp로 채팅 동시접속자 구현해보기

malangcow 2022. 9. 10.

팀 프로젝트에서 채팅에 동시접속자를 데이터를 실시간으로 보내달라는 요청이 왔다.

 

동시접속자를 어떻게 실시간으로 보내야할까.. 많은 고민을 했고 구글링을 해봤는데 자료가 거의..없었다.

 

생각해본 문제는 이정도였다.

1. 실시간으로 빠르게 바뀌는 데이터다 보니 웹소켓으로 전달해줘야 한다.

2. 일단 소켓에 연결된 세션의 수를 알아내야한다.

3. 접속자가 새로 들어올 때와 기존 접속자가 나갈 때마다 데이터를 전달해야한다.

 

1. 실시간으로 빠르게 바뀌는 데이터다 보니 웹소켓으로 전달해줘야 한다.

1번의 경우는 이미 소켓에 연결되어있는 상태이기 때문에 새 subscribe 채널만 제공해주면 된다.

function connect() {
    var socket = new SockJS('http://localhost:8080/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    // 소켓연결

    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);

        stompClient.subscribe('/topic/chats', function (chat) { // 기존 채팅 채널
            const parse = JSON.parse(chat.body);
            showGreeting(parse.name, parse.message, parse.time);
        });

        stompClient.subscribe('/topic/sessionNumbers', function (number) { //새 채널 구독
            console.log(number);
        });
    });
}

프론트에서 /topic/sessionNumbers를 추가 구독하게하고 서버에서 저 채널로 데이터를 보내주기만 하면 된다.

 

2. 일단 소켓에 연결된 세션의 수를 알아내야한다.

이부분이 상당히 걸림똘이었는데 stomp를 사용하지 않고 깡구현 했을 때는 Map으로 session들을 직접 다뤄서 구하기 쉬웠는데 stomp를 사용하면 내부로직에서 관리해주기 때문에 어딨는지 알 수 없었다.

 

근데 프로젝트 로그에 종종 세션연결에 대한 요약정리(?) 로그를 띄워주는걸 볼 수 있었는데

INFO 10356 --- [MessageBroker-1] o.s.w.s.c.WebSocketMessageBrokerStats    : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, ....//

WebSocketMessageBrokerStats 클래스에서 세션에 대한 데이터를 얻을 수 있나 했더니 

 

WebSocketMessageBrokerStats

이친구는 정말 String 타입의 요약 정리 메시지만 갖고 있었다.

물론 매번 문자열을 파싱해서 얻을 수도 있지만 뭔가 찝찝하다.

보니까 세션에 대한 관리는 멤버변수 SubProtocolWebSocketHandler webSocketHandler 이곳에 있었다.

 

WebSocketMessageBrokerStats이 친구가 생성될 때 코드를 쭉 따라가서 

어디서 주입되는지 확인하고

빈으로 제공해주는 것을 확인할 수 있었다..          근데 업캐스팅해서 제공해주네..

 

private final SubProtocolWebSocketHandler subProtocolWebSocketHandler;
private final SimpMessageSendingOperations simpMessageSendingOperations;

public SessionEventListener(WebSocketHandler webSocketHandler,SimpMessageSendingOperations simpMessageSendingOperations) {
    this.subProtocolWebSocketHandler = (SubProtocolWebSocketHandler) webSocketHandler;
    this.simpMessageSendingOperations = simpMessageSendingOperations;
}

그래서 그냥 야매로 받은 다음 다운캐스팅해서 저장하고 쓰기로 했다....   (동작은 잘 된다.)

 

SubProtocolWebSocketHandler의 필드에 있는 DefaultStats에 각종 세션 정보들이 다 담겨있고

subProtocolWebSocketHandler.getStats().getWebSocketSessions()를 호출하면 드디어 연결된 세션의 수를 구할 수 있다.

private class DefaultStats implements Stats {
    private final AtomicInteger total = new AtomicInteger();

    private final AtomicInteger webSocket = new AtomicInteger();

    private final AtomicInteger httpStreaming = new AtomicInteger();

    private final AtomicInteger httpPolling = new AtomicInteger();

    private final AtomicInteger limitExceeded = new AtomicInteger();

    private final AtomicInteger noMessagesReceived = new AtomicInteger();

    private final AtomicInteger transportError = new AtomicInteger();
    
    @Override
    public int getTotalSessions() {
        return this.total.get();
    }

    @Override
    public int getWebSocketSessions() {
        return this.webSocket.get();
    }

 

 

3. 접속자가 새로 들어올 때와 기존 접속자가 나갈 때마다 데이터를 전달해야한다.

STOMP messaging은 ApplicationContext 이벤트를 발생시키는데, 이러한 이벤트들을 처리하기 위해 Spring 관리 component 는 ApplicationListener 를 구현할 수 있다. 

 

다음과 같은 이벤트들이 존재하는데

 

  • BrokerAvailabilityEvent — broker 가 available/inavailable 될 때 발생.
  • SessionConnectEvent —  세션에 연결할 때 (?) 발생.
  • SessionConnectedEvent —  세션에 연결됐을 때 발생.
  • SessionDisconnectEvent — STOMP session 이 끝났을 때 발생.

 

음.. 두 개를 사용하면 될 것 같은데 SessionConnectEvent 와 SessionConnectedEvent 가 애매하다.

 

SessionConnectEvent 

  • 새로운 클라이언트 session 을 나타내는 STOMP CONNECT 가 수신될 때 나타난다.
  • 이 이벤트는 session id , user information 및 client 가 보냈을 지 모르는 어떤 사용자 정의 header 를 포함한다.
    이것은 client sessions 을 추적하는데 유용하다.
  • 이 이벤트에 subscribe 하는 Components 는 SimpMessageHeaderAccessor 나 StompMessageHeaderAccessor 를 사용하여 메시지를 wrapping 할 수 있다. 

SessionConnectedEvent 

  • broker 가 SessionConnectEvent 이후 CONNECT 에 대한 응답으로 STOMP CONNECTED frame 을 전송했을 때 짧게 발생한다. 
  • 이 시점에 STOMP session 은 완전히 연결되었음으로 간주될 수 있다. 

 

설명을 보아하니 SessionConnectEvent  이후 SessionConnectedEvent 가 동작하는 것으로 보이고 그 후 STOMP session 은 완전히 연결되었다고 간주한다고 하는걸로 보아 SessionConnectedEvent 를 사용해야 할 것 같다.

 

이제 이벤트 리스너를 등록하는 방법을 알아보자.

 

ApplicationListener interface를 구현해서 등록하는 방법.

@Component
public class SessionEventListener implements ApplicationListener<SessionConnectedEvent> {

    private final SubProtocolWebSocketHandler subProtocolWebSocketHandler;
    private final SimpMessageSendingOperations simpMessageSendingOperations;

    public SessionEventListener(WebSocketHandler webSocketHandler,SimpMessageSendingOperations simpMessageSendingOperations) {
        this.subProtocolWebSocketHandler = (SubProtocolWebSocketHandler) webSocketHandler;
        this.simpMessageSendingOperations = simpMessageSendingOperations;
    }

    @Override
    public void onApplicationEvent(SessionConnectedEvent event) {
        int webSocketSessions = subProtocolWebSocketHandler.getStats().getWebSocketSessions();
        
        simpMessageSendingOperations.convertAndSend("/topic/number",webSocketSessions);
    }
}

 

하지만 스프링 4.2부터 annotation 방식을 제공해준다고한다.

 

빈으로 등록될 클래스 안에 메소드 방식으로 간단하게 구현이 가능하다.

@EventListener
public void sessionDisconnectEventHandler(SessionDisconnectEvent event) {
    System.out.println("SessionDisconnectEvent = " + event);
}

좀 더 찾아보니

이러한 이벤트 핸들러는 2개 이상일 수 있고 싱글 스레드에서 순차적으로 발생되나, 순서는 보장할 수 없다.

  • @Order를 붙여줘서 순서를 정해줄 수 있다.
  • @Async를 붙여주면 비동기적으로 실행할 수도 있다. 단 applicaitonRunner에 @EnableAsync도 붙여줘야 멀티스레드 환경에서 돌아간다.

 

 

채팅기능에서 동시접속자 기능이 성공적으로 구현됐다. 채팅이 좀 더 풍족해진 느낌이다.!!

'spring' 카테고리의 다른 글

junit5 repository 테스트 데이터 한 번만 setup(insert)하기  (0) 2022.09.18

댓글