[캡스톤디자인] Spring Boot, STOMP를 이용한 Chatting 구현 (1)

Dev_Sanizzang·2023년 5월 4일
0

캡스톤디자인

목록 보기
4/15

📕 개요

  • 사용자 인증 부분 구현을 마치고 이제 온라인 모임 서비스에 필요한 채팅구현을 맡게 되었다.
  • Chatting은 처음 구현해보는지라 일단 배경지식에 대해 공부가 필요해 보인다!

🤔 WebSocket, STOMP가 뭐야?

먼저 Chatting을 구현하기 위해서는 WebSocket이라는 것과 STOMP라는 것에 대한 이해가 필요해 보였다.

그래서 나는 WebSocket과 STOMP에 대한 자료들을 찾아봤다.

구글링을 해본결과 아래 유튜브가 제일 정리가 잘 되어있었다!

💡 [10분 테코톡] 아론의 웹소켓&스프링
https://www.youtube.com/watch?v=rvss-_t6gzg

위의 영상을 정리해보자

📌 Web Socket?

  • 전이중 통신 채널을 제공해서 실시간성 보장
  • 게임, 채팅, 실시간 주식 거래 사이트에서 사용

HTTP에서도 실시간성을 보장하는 기법(Polling, Long Polling, Streaming)이 존재한다.

HTTP는 비 연결성으로 매번 연결을 맺고 끊는 과정의 비용이 든다.
허나 웹소켓은 연결 지향으로 한번 연결을 맺으면 데이터를 양방향으로 계속 보내줄 수 있다.

또한 위와 같이 HTTP는 매 요청을 보낼 때마다 많은 정보들을 보내야 되지만
웹소켓은 HTTP로 연결이 수립되고 나면 간단한 메시지로 통신이 가능하다.

웹소켓을 지원하지 않는 브라우저는 어떻게하나?

💡 웹소켓을 지원하지 않는 환경에서도 SockJS, Socket.io를 통해 해결할 수 있다.
(해당 브라우저가 웹소켓을 지원하는지 확인을 해보고 그렇지 않은 경우 대안책으로 그 기법들을 대신 사용한다.)

📜 Spring-WebSocket 구현코드

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketTextHandler(), "/user")
                .setAllowedOrigins("*")
                .withSockJS();
    }
}
  • 웹소켓에 대한 Configuration 클래스를 만들고 WebSocketConfigurer 인터페이스 구현하고, @EnableWebSocket 어노테이션을 달아준다.

  • 스프링에서 웹소켓을 사용하기 위해서 클라이언트가 보내는 통신을 처리할 핸들러가 필요하다
    -> 직접 구현한 웹소켓 핸들러 (SocketTextHandler)를 웹소켓이 연결될 때, Handshake할 주소 (/user)와 함께 addHandler 메소드의 인자로 넣어준다.

  • setAllowedOrigins("*") 으로 Cors 설정을 할 수 있다.

  • 스프링에서 웹소켓을 사용할 때, same-origin만 허용하는 것이 기본정책이다.

  • withSockJS() 으로 SockJS 라이브러리를 사용하도록 설정할 수 있다.

import org.json.JSONObject;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

public class SocketTextHandler extends TextWebSocketHandler {

    private final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        String payload = message.getPayload();
        JSONObject jsonObject = new JSONObject(payload);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("Hi " + jsonObject.getString("user") + "!"));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
    }
}
  • 간단한 웹소켓 핸들러이다.

  • 웹소켓 프로토콜은 기본적으로 Text, Binary 타입을 지원한다.
    -> 필요에 따라 TextWebSocketHandler, BinaryWebSocketHandler를 상속하여 구현해주면 된다.

  • WebSocketSession 파라미터는 웹소켓이 연결될 때 생기는 연결정보를 담고 있는 객체이다.
    -> Handler에서는 웹소켓 통신에 대한 처리를 위해, 웹소켓 세션들을 컬랙션에 담아 관리하는 경우가 많다

  • 웹소켓 커넥션이 맺어지는 경우 (afterConnectionEstablished) -> sessions.add(session);
    커넥션이 끊어지면 (afterConnectionClosed) -> sessions.remove(session);

  • 웹소켓 세션을 통해, 연결된 모든 클라이언트들에게 메시지를 보낼 수 있다.

    📌 Stomp

💡 스프링부트에서 WebSocket 의존성을 받아오면 Spring Messaging이 같이 달려온다. 그럼 Spring Messaging은 무엇일까? 그전에 STOMP 프로토콜에 대해서 이해해야 한다.

  • STOMP는 간단한 텍스트 기반 메시징 프로토콜이다.
    -> 메시지 브로커라는 것을 활용하여 pub/sub 방식으로 클라이언트와 서버가 쉽게 메시지를 주고 받을 수 있도록하는 프로토콜이다.

pub/sub 은 일종의 메시지 패러다임이다.

발신자가 어떠한 범주(예를 들어 경로)로 메시지를 발행하면
이 범주를 "구독"하고 있는 수신자들이 해당 메시지를 받아볼 수 있는 방식이다.
메시지 브로커는 발신자가 보낸 메시지들을 받아서 수신자들에게 전달해주는 것이다.

  • STOMP 프로토콜은 웹소켓만을 위해 만들어진 프로토콜이 아니다.

  • 중요한 점은 웹소켓과 같이 몇몇 양방향 통신 프로토콜에서 STOMP를 함께 사용할 수 있다는 것이다.

  • Spring은 웹소켓 위에 STOMP 프로토콜을 얹어 사용하는 방법을 지원해준다.

❗ 그럼 굳이 웹소켓위에 STOMP를 얹어서 사용하는 이유가 무엇일까?

  • 웹소켓은 Text, Binary 타입의 메시지를 양방향으로 주고받을 수 있는 프로토콜이다. 하지만 메시지를 주고 받는 형식이 따로 정해진 것이 없다.
  • 웹소켓만 사용하는 프로젝트가 커지면, 주고 받는 메시지에 대한 형식이 중요하게 된다.
  • 정의된 메시지 형식대로 파싱하는 로직 또한 따로 구현해야한다.

    💡 STOMP를 사용하면 메시지 형식에 대한 고민과 파싱 로직을 위한 코드 구현이 필요없어진다.

  • 웹소켓만 사용할 때(왼쪽) 오고 가는 데이터는 오직 날것의 메시지 뿐이다.
  • STOMP를 사용할 때(오른쪽)는 커맨드, 헤더, 바디의 형태로 데이터가 오고간다.

Spring에서의 STOMP 동작 흐름

  • 메시지를 보내려는 발신자, 메시지를 받으려는 구독자가 있다.
    구독자는 /topic 이라는 경로를 구독하고 있다.

  • 발신자는 /topic을 destination 헤더로 넣어 메시지를 메시지 브로커를 통해 구독자들에게 곧바로 송신할 수 있다

  • 또는 서버 내에서 어떤 가공처리가 필요하다면 /app 경로로 메시지를 송신할 수 있다.
    -> 서버가 가공처리가 끝난 데이터를 /topic이라는 경로를 담아 메시지 브로커에게 전달하면
    -> 메시지 브로커는 전달받은 메시지를 /topic을 구독하는 구독자들에게 최종적으로 전달한다.

📜 Spring-STOMP 구현코드

package com.websocket;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/queue","/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/gs-guide-websocket")
                .withSockJS();
    }
}
  • @EnableWebSocketMessageBroker를 달고
    WebSocketMessageBrokerConfigurer 인터페이스 구현해준다.

  • configureMessageBroker: 메시지 브로커를 설정하는 메서드

    • enableSimpleBroker(): 내장 메시지 브로커 사용
      파라미터로 지정한 prefix(/queue 또는 /topic)가 붙은 메시지를 발행할 경우, 메시지 브로커가 이를 처리하게 된다.
      • /queue prefix는 메시지가 1대1로 송신될 때,
        /topic prefix는 메시지가 1대다로 브로드캐스팅될 때 사용하는게 컨밴션이다.
    • setApplicationDestinationPrefixes() : 메시지 핸들러로 라우팅되는 prefix(/app)를 파라미터로 지정할 수 있다.
      -> 메시지 가공 처리가 필요한 경우, 가공 핸들러로 메시지를 라우팅 되도록하는 설정이다.
  • registerStompEndpoints: 웹소켓 코드에서 봤던 addHandler 메서드와 굉장히 비슷하다.
    -> 인자로 들어가는 경로는 웹소켓의 /user처럼 처음 웹소켓 핸드쉐이크를 위한 주소이고
    여기에서도 뒤에 cors 설정과 sockJS 설정을 그대로 해줄 수 있다.

    • 다른점이 있다면 여기서는 handler를 설정해줄 필요가 없다.
      -> STOMP를 사용하게 되면 아까 웹소켓만을 사용했을 때와는 다르게 하나의 연결 주소마다 핸들러 클래스를 구현하고 설정해줄 필요 없이, 컨트롤러 방식으로 간편하게 사용하는 방식이기 때문이다.
package com.websocket;

import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.util.HtmlUtils;

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greeting")
    public Greeting greeting(HelloMessage message) throws Exception {
        Thread.sleep(1000);
        return new Greeting("Hello" + HtmlUtils.htmlEscape(message.getName()));
    }
  • STOMP를 사용하게 되면 따로 인터페이스를 상속을 받거나 할 필요없이 컨트롤러 어노테이션을 사용할 수 있다.

  • @MessageMapping: 기존에 알던 requestMapping과 비슷한 역할을 함
    -> STOMP 웹소켓 통신을 통해 메시지가 들어왔을 때도 메시지의 destination 헤더와 MessageMapping에 설정된 경로가 일치하는 핸들러를 찾고 해당 핸들러가 처리를 하게된다.
    여기서는 아까 Configuration에서 설정해둔 /app이라는 prefix와 합쳐져서
    /app/hello destination 헤더를 가진 메시지들이 이 핸들러를 거치게 된다.

  • @SendTo: 핸들러에서 처리를 마친 후 반환 값을 /topic/greeting의 경로로 메시지를 보내준다.
    -> 여기서는 처리를 마치고 반환된 Greeting 객체를 /topic/greeting 경로로 다시 보내는 것이니 바로 Simple 브로커로 전달될 것이다.

STOMP를 사용하는 장점

  1. 하위 프로토콜, 컨벤션을 따로 정의할 필요 없다.
    -> STOMP가 프레임 단위로 정의해준다.
  2. 연결 주소마다 새로운 Handler를 구현하고 설정해줄 필요 없다.
    -> @Controller 어노테이션을 사용하면 된다.
  3. 외부 Messaging Queue (Kafka, RabbitMQ, ...)를 사용할 수 있다.
    -> Spring이 기본적으로 제공하는 내장 메시지 브로커가 아닌 외부 메시지 큐를 연동해서 사용할 수 있다.
  4. Spring Security를 사용할 수 있다.
    -> 오고 가는 메시지에 대한 보안설정을 할 수 있다.

🚪 마무리

지금까지 WebSocket, STOMP에 대한 개념과 Spring에서 WebSocket과 STOMP를 사용하는 방법을 알아봤다.
다음 장에서는 Spring STOMP를 사용하여 간단한 채팅프로그램을 만들고 현재 진행중인 프로젝트에도 채팅을 적용시켜보도록 하겠다.

profile
기록을 통해 성장합니다.

0개의 댓글