Red Glitter Pointer

 

클라이언트와 서버가 통신할 때 HTTP 통신을 주로 사용한다.

HTTP 통신은 비연결성(connectionless), 무상태성(stateless), 단방향 통신이라는 특징을 가지고 있는데 이러한 HTTP 통신의 경우 채팅과 같은 실시간 통신에는 적합하지 않다.

물론 흉내는 낼 수 있으나 완벽하지는 않다. 실시간 통신이 필요한 경우 사용하는 통신을 소켓 통신 이라고 한다.

HTTP 통신과 다르게 연결을 맺고 바로 끊어버리는 것이 아니라 계속 유지하기 때문에 실시간 통신에 적합하다

 

 

 

 

 1. STOMP (Simple Text Oriented Messaging Protocol)

 

STOMP란? 

메시징 전송을 효율적으로 하기 위한 프로토콜로 PUB / SUB 기반으로 동작한다.

메시지의 발행자와 구독자가 존재하고, 메시지를 보내는 사람과 받는 사람이 구분되어 있다.

메시지 브로커는 발행자가 보낸 메시지를 구독자에게 전달해주는 역할을 한다.

 

 

STOMP 형식 

STOMP는 HTTP와 비슷하게 frame 기반 프로토콜 command, header, body로 이루어져 있다.

COMMAND
header1:value1
header2:value2

Body^@

 

  • 클라이언트는 메시지를 전송하기 위해 COMMAND로 SEND 또는 SUBSCRIBE 명령을 사용하며 header와 value로 메시지의 수신 대상과 메시지에 대한 정보를 설명할 수 있다.
  • 일반적으로는 아래의 형식을 따른다
"topic/.." -> publish-subscribe (1:N)
"queue/" -> point-to-point (1:1)


// ex) 클라이언트 A가 5번 채팅방 구독
SUBSCRIBE
destination: /topic/chat/room/5
id: sub-1

^@


// ex) 클라이언트B가 채팅 메시지 전송
SEND
destination: /pub/chat
content-type: application/json

{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@

 

  • STOMP 서버는 모든 구독자에게 메시지를 브로드캐스팅(BroadCasting)하기 위해 MESSAGE COMMAND를 사용할 수 있다.
  • 서버는 내용을 기반으로 메시지를 전송할 브로커에게 전달한다.
MESSAGE
destination: /topic/chat/room/5
message-id: d4c0d7f6-1
subscription: sub-1

{"chatRoomId": 5, "type": "MESSAGE", "writer": "clientB"} ^@

 

  • 서버는 불분명한 메시지를 전송할 수 없기 때문에 서버의 모든 메시지는 특정 클라이언트 구독에 응답해야 하고, 서버 메시지의 "subscription-id" 헤더는 클라이언트 구독 id 헤더와 일치해야 한다.

 

 

 

STOMP의 장점 

WebSocket만 사용해서 구현하면 해당 메시지가 어떤 요청인지, 어떤 포맷인지, 메시지 통신 과정을 어떻게 처리해야 하는지 정해져 있지 않아 일일이 구현해야 한다.

 

STOMP를 서브 프로토콜로 사용하면, 클라이언트와 서버가 서로 통신할 때 메시지의 형식, 유형, 내용 등을 정의해준다. 따라서 메시지 송수신에 대한 처리를 명확하게 정의할 수 있고 WebSocketHandler를 직접 구현할 필요 없이, 어노테이션을 사용하여 메시지 발행 시 엔드포인트를 별도로 분리하여 관리할 수 있다.

 

 

 

 

 2. Spring WebSocket STOMP

 

 

Getting Started | Using WebSocket to build an interactive web application

In Spring’s approach to working with STOMP messaging, STOMP messages can be routed to @Controller classes. For example, the GreetingController (from src/main/java/com/example/messagingstompwebsocket/GreetingController.java) is mapped to handle messages t

spring.io

 

의존성 주입 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-websocket'
}

 

 

 

WebSocket STOMP 설정 

WebSocketConfig.java
package com.example.messagingstompwebsocket;

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 WebSocketConfig implements WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) { // ④
    config.enableSimpleBroker("/topic"); // ⑤
    config.setApplicationDestinationPrefixes("/app"); // ⑥
  }

  @Override
  public void registerStompEndpoints(StompEndpointRegistry registry) { // ②
    registry.addEndpoint("/chattings"); // ③
  }

}

 

① 메시지 브로커가 지원하는 WebSocket 메시지 처리를 활성화

② HandShake와 통신을 담당할 EndPoint 지정

③ WebSocket 연결 시 요청을 보낼 endPoint 지정

④ 메모리 기반의 Simple Message Broker 활성화

⑤ 메시지 브로커가 Subscriber들에게 메시지를 전달할 URL 지정(메시지 구독 요청) - 발행자가 "/subscription" 의 경로로 메시지를 주면 구독자들에게 전달.

⑥ 클라이언트가 서버로 메시지 보낼 URL 접두사 지정(메시지 발행 요청) - 발행자가 "/publication" 의 경로로 메시지를 주면 가공해서 구독자들에게 전달. 

 

 

 

메시지 브로커

① 구독자는 특정 topic을 구독하고 발행자는 해당 topic으로 메시지를 날린다

② 메시지 브로커는 이 메시지를 관리하여 이를 구독 중인 구독자들에게 메시지를 보내준다.

③ 메시지를 바로 전달하는 것이 아니라 중간에 존재하는 메시지 브로커에게 전달하고 이 브로커가 구독자들에게 전달해주는 형태이다.

 

 

 

enableSimpleBroker

스프링 document에서 설명을 보면

Enable a simple message broker and configure one or more prefixes to filter destinations targeting the broker (e.g.

 

1. simple message broker을 활성화한다.

2. 브로커를 타겟으로하는 하나 이상의 접두사를 구성한다.

 

"/topic" 접두사가 붙은 경로는 브로커를 타겟으로 한다는 설정을 한 것이다.

 

🌟 즉, "/topic/..." 경로로 메시지를 보내면 이 메시지는 브로커를 향하게 되고, 브로커는 이 경로를 구독중인 구독자들에게 메시지를 발송한다.

 

 

 

 

setApplicationDestinationPrefixes

스프링 document에서 설명을 보면

Configure one or more prefixes to filter destinations targeting application annotated methods.

 

1. application annotated method를 타겟으로 하는 하나 이상의 접두사를 구성한다.

 

"/app" 접두사가 붙은 경로는 @MessageMapping 어노테이션이 붙은 곳을 타겟으로 한다는 설정을 한 것이다.

 

🌟 "/app/..." 경로로 메시지를 보내면 이 메시지는 @MessageMapping 어노테이션이 붙은 곳으로 향하게 된다.

 

@MessageMapping으로 이동하게 되면 그 곳에서 이 메시지를 가공할 수 있게 된다.

또한 @SendTo 어노테이션을 사용하여 메시지가 향할 곳을 정할 수 있다.

 

 

 

 

ChattingController 작성 

ChattingController.java
import com.jeongyuneo.springwebsocket.dto.ChattingRequest;
import com.jeongyuneo.springwebsocket.service.ChattingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;

@Slf4j
@RequiredArgsConstructor
@Controller
public class ChattingController {

    private final SimpMessagingTemplate simpMessagingTemplate;  // ①

    @MessageMapping("/chattings/{chattingRoomId}/messages")  // ②
    public void chat(@DestinationVariable Long chattingRoomId, ChattingRequest chattingRequest) {  // ③
        simpMessagingTemplate.convertAndSend("/subscription/chattings/" + chattingRoomId, chattingRequest.getContent());
        log.info("Message [{}] send by member: {} to chatting room: {}", chattingRequest.getContent(), chattingRequest.getSenderId(), chattingRoomId);
    }
}

 

① @EnableWebSocketMessageBroker를 통해 등록되는 Bean으로, Broker로 메시지 전달

② 클라이언트가 SEND할 수 있는 경로

WebSocketConfig에서 등록한 applicationDestinationPrefixes와 @MessageMapping의 경로가 합쳐진다.

③ 클라이언트에서 /publication/chattings/{chattingRoomId}/messages 로 메시지를 보내면 해당 채팅방을 구독 중인 사용자들에게 메시지를 전달

@DestinationVariable은 구독 및 발행 URL의 경로 변수를 지정한다.

 

 

ChattingRequest.java
import lombok.*;

@Getter
@Builder
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ChattingRequest {

    private Long senderId;
    private String content;
}

 

 

 

📌 메시지를 전송하는 과정 정리

 

① "/topic"으로 시작하는 경로는 메시지 브로커를 향하도록 설정한다.

② "/app"으로 시작하는 경로는 @MessageMapping을 향하도록 설정한다.

③ 구독자는 "/topic/greetings"으로 시작하는 경로를 구독한다. (1번 설정 때문에)

④ 발행자는 "/app/hello"로 시작하는 경로로 메시지를 보낸다.

⑤ 2번 설정 때문에 @MessageMapping("/hello")가 붙어있는 곳으로 메시지가 간다.

⑥ 메시지 가공이 끝난 뒤 ("/topic/greetings") 로 보낸다.

⑦ 1번 설정 때문에 이 메시지는 메시지 브로커로 가게 된다.

⑧ 메시지 브로커에서 "/topic/greetings"를 구독 중인 구독자들에게 메시지를 전송한다.

 

 

 

 

 

 

 

 

 

🌐 참고 링크

https://eoneunal.tistory.com/17

https://growth-coder.tistory.com/157

 

 

+ Recent posts

loading