본문 바로가기

백엔드

채팅 애플리케이션 (2) - STOMP

https://github.com/fineman999/simple-stomp-spring

 

GitHub - fineman999/simple-stomp-spring: spring, stomp, message broker, mysql 을 이용한 채팅 애플리케이션

spring, stomp, message broker, mysql 을 이용한 채팅 애플리케이션 - GitHub - fineman999/simple-stomp-spring: spring, stomp, message broker, mysql 을 이용한 채팅 애플리케이션

github.com


 

목차

    서론

    지난 시간에 채팅 애플리케이션을 만들기 위한 기본적인 지식을 학습하였다. 이번 시간에는 STOMP에 대해 학습하고 Spring과 STOMP를 이용해 실시간 통신을 해보자.

     

    STOMP

    웹소켓 프로토콜은 두 가지 유형의 메시지(텍스트와 바이너리)를 정의하지만, 그 내용은 정의되어 있지 않다. 이 프로토콜은 클라이언트와 서버가 각각 보낼 수 있는 메시지의 종류, 형식, 각 메시지의 내용 등을 정의하기 위해 웹소켓 위에 사용할 하위 프로토콜(즉, 상위 수준의 메시징 프로토콜)을 협상할 수 있는 메커니즘을 정의한다. 하위 프로토콜을 사용하는 것은 선택 사항이지만 어느 쪽이든 클라이언트와 서버는 메시지 내용을 정의하는 프로토콜에 동의해야 한다.

     

    Frame

    STOMP에서 프레임(Frame)은 STOMP 프로토콜 메시지의 기본 단위를 나타낸다. STOMP는 클라이언트와 서버 간에 주고받는 메시지를 프레임이라고 불리는 형식으로 정의한다.

    STOMP 프레임은 크게 세 가지 요소로 구성된다.

     

    명령(Commands)

    명령은 프레임의 타입을 나타낸다. 예를 들어 CONNECT, SEND, SUBSCRIBE, UNSUBSCRIBE, DISCONNECT 등이 명령에 해당된다.(추후 설명)

    • 명령은 STOMP 프레임의 첫 번째 줄에 위치하며, 이 명령에 따라 프레임의 목적과 역할이 결정된다.

    헤더(Headers)

    • 헤더는 키-값 쌍으로 이루어져 있으며, STOMP 메시지에 대한 부가적인 정보를 포함한다. 헤더는 명령 다음에 위치하며, 각 헤더는 키와 값으로 이루어져 있다. 예를 들어, `destination:/app/chat`, `content-type:text/plain` 등이 헤더에 해당된다.
    • 또한 사용자 정의 헤더도 정의할 수 있다.

    바디(Body)

    • 바디는 메시지의 실제 내용을 나타낸다. 헤더 다음에 빈 줄을 포함한 후에 바디가 위치하며, 이 부분에 대한 실제 메시지 데이터가 들어간다. 바디는 텍스트 또는 바이너리 데이터일 수 있다.

    일반적으로 STOMP 프레임의 구조는 다음과 같다.

     

    COMMAND
    header1:value1
    header2:value2
    
    Body^@

    여기서 프레임은 end-of-line(EOL)으로 끝나는 명령 문자열(COMMAND)로 시작하며, 명령 뒤에는 <key>:<value> 형식의 헤더 항목이 2개 있다. 각 헤더 항목은 EOL로 종료된다. 빈 줄(즉, 추가 EOL)은 헤더의 끝과 본문의 시작을 나타낸다. 본문 뒤에는 NULL이 이어진다. 여기에서는 ^@, control-@(ASCII)를 사용하여 NULL을 나타낸다. 선택적으로 NULL 뒤에 여러 개의 EOL이 올 수 있다.

     

    Connecting

    STOMP 클라이언트는 CONNECT 프레임을 전송하여 서버에 스트림 또는 TCP 연결을 시작한다.

     

    CONNECT
    accept-version:1.2
    host:stomp.github.org
    
    ^@

    서버가 연결 시도를 수락하면 연결됨 프레임으로 응답한다.

     

    CONNECTED
    version:1.2
    
    ^@

    서버는 모든 연결 시도를 거부할 수 있다. 서버는 연결이 거부된 이유를 설명하는 오류 프레임으로 응답한 다음 연결을 닫아야 한다.

     

    Client Frames

    Client 프레임 종류들로 SEND, SUBSCRIBE, UNSUBSCRIBE, BEGIN, COMMIT, ABORT, ACK, NACK, DISCONNECT 등이 있다. 이번 시간에는 SEND, SUBSCRIBE, UNSUBSCRIBE, DISCOONECT을 알아보자.

     

    SEND

    SEND
    destination:/queue/a
    content-type:text/plain
    
    hello queue a
    ^@

    SEND 프레임은 메시징 시스템의 대상에게 메시지를 보낸다. 이 프레임에는 메시지를 보낼 Destination를 나타내는 REQUIRED 헤더인 대상 하나가 있다. SEND 프레임의 본문은 전송할 메시지이다.

    SEND 프레임에는 본문이 있는 경우 콘텐츠 길이 헤더와 콘텐츠 유형 헤더가 포함되어야 한다.

     

    SUBSCRIBE

    SUBSCRIBE 프레임은 지정된 대상을 수신하도록 등록하는 데 사용된다. SEND 프레임과 마찬가지로 SUBSCRIBE 프레임에는 클라이언트가 구독하려는 대상을 나타내는 대상 헤더가 필요하다. 구독한 대상에 수신된 모든 메시지는 이제 서버에서 클라이언트로 MESSAGE 프레임으로 전달된다.

     

    SUBSCRIBE
    id:0
    destination:/queue/foo
    
    ^@

    STOMP 서버는 Subscribe 명령어에 추가 서버별 헤더를 지원할 수 있다.

     

    SUBSCRIBE id Header

    단일 연결에 서버와 여러 개의 열려 있는 구독이 있을 수 있으므로, 구독을 고유하게 식별하려면 프레임에 ID 헤더를 포함해야 한다. ID 헤더를 통해 클라이언트와 서버는 후속 메시지 또는 구독 취소 프레임을 원래 구독과 연관시킬 수 있다.
    동일한 연결 내에서 서로 다른 구독은 반드시 서로 다른 구독 식별자를 사용해야 한다.

     

    UNSUBSCRIBE

    UNSUBSCRIBE 프레임은 기존 SUBSCRIBE을 제거하는 데 사용된다. SUBSCRIBE가 제거되면 STOMP 연결은 더 이상 해당 SUBSCRIBE에서 메시지를 수신하지 않는다.

    단일 연결에 서버에 대해 열려 있는 여러 개의 SUBSCRIBE가 있을 수 있으므로 제거할 SUBSCRIBE을 고유하게 식별할 수 있는 ID 헤더가 프레임에 포함되어야 한다. 이 헤더는 기존 구독의 구독 식별자와 일치해야 한다.

    UNSUBSCRIBE
    id:0
    
    ^@

     

    DISCONNECT

    클라이언트는 언제든지 소켓을 닫아 서버와의 연결을 끊을 수 있지만 이전에 전송한 프레임이 서버에 수신되었다는 보장은 없다. 클라이언트가 이전의 모든 프레임을 서버에서 수신했다는 것을 보장하는 점진적 종료를 수행하려면 클라이언트가 서버를 종료해야 한다:

    수신 헤더가 설정된 연결 해제 프레임을 보내야 한다.

    DISCONNECT
    receipt:77
    ^@

    서버가 소켓 끝을 너무 빨리 닫으면 클라이언트가 예상되는 수신 프레임을 수신하지 못할 수 있다.

     

    Server Frames

    서버는 때때로 클라이언트에 프레임을 전송한다(초기 연결 프레임 외에).

    Server Frames 종류로 MESSAGE, RECEIPT, ERROR 등이 있다. 이 중 MESSAGE, ERROR에 대해 설명한다.

     

    MESSAGE

    MESSAGE 프레임은 SUBSCRIBE에서 클라이언트로 메시지를 전달하는 데 사용된다.
    MESSAGE 프레임에는 메시지가 전송된 대상을 나타내는 대상 헤더가 포함되어야 한다. 메시지가 STOMP를 사용하여 전송된 경우 이 대상 헤더는 해당 SEND 프레임에 사용된 헤더와 동일해야 한다.
    또한 MESSAGE 프레임에는 해당 메시지의 고유 식별자가 포함된 message-id 헤더와 메시지를 수신하는 SUBSCRIBE의 식별자와 일치하는 subscription 헤더가 포함되어야 한다.
    프레임 본문에는 메시지 내용이 포함된다.

     

    MESSAGE
    subscription:0
    message-id:007
    destination:/queue/a
    content-type:text/plain
    
    hello queue a^@

     

    메시지 프레임에는 콘텐츠 길이 헤더와 본문이 있는 경우 콘텐츠 유형 헤더가 포함되어야 한다.
    또한 메시지 프레임에는 프레임에 추가될 수 있는 서버별 헤더 외에 메시지가 대상에게 전송될 때 존재했던 모든 사용자 정의 헤더가 포함된다.

     

    ERROR

    문제가 발생하면 서버가 오류 프레임을 보낼 수 있다. 이 경우 오류 프레임을 보낸 직후 연결을 닫아야 한다.
    오류 프레임에는 오류에 대한 간단한 설명이 포함된 메시지 헤더가 포함되어야 하며, 본문에는 더 자세한 정보가 포함될 수 있다(또는 비어 있을 수 있음).

     

    ERROR
    receipt-id:message-12345
    content-type:text/plain
    content-length:170
    message:malformed frame received
    
    The message:
    -----
    MESSAGE
    destined:/queue/a
    receipt:message-12345
    
    Hello queue a!
    -----
    Did not contain a destination header, which is REQUIRED
    for message propagation.
    ^@

    오류가 클라이언트에서 전송된 특정 프레임과 관련된 경우 서버는 오류를 일으킨 원본 프레임을 식별하는 데 도움이 되도록 추가 헤더를 추가해야 한다.
    오류 프레임에는 본문이 있는 경우 콘텐츠 길이 헤더와 콘텐츠 유형 헤더가 포함되어야 한다.

     

    개요

    • STOMP(단순 텍스트 지향 메시징 프로토콜)는 원래 스크립트 언어(예: 루비, 파이썬, Perl)가 엔터프라이즈 메시지 브로커에 연결하기 위해 만들어졌다. 일반적으로 사용되는 메시징 패턴의 최소한의 하위 집합을 처리하도록 설계되었다.
    • STOMP는 TCP 및 WebSocket과 같은 신뢰할 수 있는 양방향 스트리밍 네트워크 프로토콜을 통해 사용할 수 있다. STOMP는 텍스트 지향 프로토콜이지만 메시지 페이로드는 텍스트 또는 바이너리일 수 있다.
    • STOMP는 프레임 기반 프로토콜로, 프레임은 HTTP를 기반으로 모델링된다. 다음 목록은 STOMP 프레임의 구조를 보여준다.
    COMMAND
    header1:value1
    header2:value2
    
    Body^@

    프레임은 줄 끝(EOL: End of Line)으로 끝나는 명령 문자열로 시작한다. 명령 뒤에는 <key>:<value> 형식의 헤더 항목이 0개 이상 있다. 각 헤더 항목은 EOL로 종료된다. 빈 줄(즉, 추가 EOL)은 헤더의 끝과 본문의 시작을 뜻한다. 본문 뒤에는 NULL octet(8개의 비트)을 나타낸다. 이 예제에서는 ^@를 이용하여 NULL octet을 나타낸다. 선택적으로 NULL octet 뒤에 여러 개의 EOL이 올 수 있다.

    또한 대소문자를 구분한다.

     

    프로젝트 생성

    build.gradle

    ...
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-web'
        implementation 'org.springframework.boot:spring-boot-starter-websocket'
        compileOnly 'org.projectlombok:lombok'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
    }
    ...
    • spring-boot-starter-web: Spring Boot에서 제공하는 웹 애플리케이션을 개발하기 위한 기본 의존성이다. 이 의존성은 웹 애플리케이션을 위한 Spring MVC 및 내장된 웹서버(Tomcat, Jetty, 등)를 설정한다.
    • spring-boot-starter-websocket: Spring Boot에서 제공하는 WebSocket을 사용하기 위한 의존성이다. WebSocket은 양방향 통신을 지원하여 실시간으로 데이터를 주고 받을 수 있는 프로토콜을 제공한다.
    • lombok: Lombok은 자바 소스 코드에서 반복적으로 사용되는 코드를 자동으로 생성해주는 라이브러리다. 런타임 시에 리플렉션을 이용하여 코드를 간결하게 해 준다.

    Spring Stomp - Flow of Messages

    STOMP 엔드포인트가 노출되면 Spring 애플리케이션은 연결된 클라이언트를 위한 STOMP 브로커가 된다.

    spring-messaging 모듈에는 Spring Integration(통합)에서 시작된 메시징 애플리케이션에 대한 기본 지원이 포함되어 있으며, 나중에 많은 Spring 프로젝트 및 애플리케이션 시나리오에서 더 광범위하게 사용하기 위해 추출되어 Spring 프레임워크에 통합되었다.

    • 사용 가능한 메시징 추상화
      • Message: 헤더와 페이로드를 포함한 메시지의 간단한 표현
      • MessageHandler: 메시지 처리를 위한 컨트렉트
      • MessageChannel: 생산자와 소비자 간의 느슨한 결합을 가능하게 하는 메시지를 전송하기 위한 컨트렉트
      • SubscribableChannel: MessageHandler 구독자들이 있는 MessageChannel
      • ExecutorSubscribableChannel: 메시지를 전달하기 위해 Executor를 사용하는 SubscribableChannel이다.

    @EnableWebSocketMessageBroker은 모두 앞의 구성 요소를 사용하여 메시지 워크플로를 조립한다. 다음 다이어그램은 간단한 기본 제공 메시지 브로커가 활성화될 때 사용되는 구성 요소를 보여준다.

     

    • 세 개의 메시지 채널이 나와 있다:
      • clientInboundChannel(request channel): 웹소켓 클라이언트로부터 받은 메시지를 전달하기 위한 채널
      • clientOutboundChannel(response channel): 서버 메시지를 웹소켓 클라이언트로 보내기 위한 채널
      • brokerChannel: 서버 측 애플리케이션 코드 내에서 메시지 브로커로 메시지를 보내기 위한 채널
    • 다음 다이어그램은 구독 관리 및 메시지 브로드캐스팅을 위해 외부 브로커(예: RabbitMQ)를 구성할 때 사용되는 구성 요소를 보여준다.

     

    • 앞의 두 다이어그램의 주요 차이점은 TCP를 통해 외부 STOMP 브로커로 메시지를 전달하고 브로커에서 가입한 클라이언트로 메시지를 전달할 때 BrokerRelay를 사용한다는 것이다.
    • 웹 소켓 연결에서 메시지가 수신되면 STOMP 프레임으로 디코딩되어 Spring 메시지 표현으로 변환된 후 추가 처리를 위해 clientInboundChannel로 전송된다. 예를 들어, 대상 헤더가 /app로 시작하는 STOMP 메시지는 애노테이션이 달린 컨트롤러의 @MessageMapping 메서드로 라우팅 될 수 있고, /topic 및 /queue 메시지는 메시지 브로커로 직접 라우팅될 수 있다. 참고로 /topic은 1대 다, /queue는 1대 1을 의미한다.
    • 클라이언트의 STOMP 메시지를 처리하는 주석이 달린 @Controller는 brokerChannel을 통해 메시지 브로커에 메시지를 보낼 수 있으며, 브로커는 clientOutboundChannel을 통해 일치하는 구독자에게 메시지를 브로드캐스팅한다. 동일한 컨트롤러가 HTTP 요청에 대한 응답으로 동일한 컨트롤러가 HTTP 요청에 대한 응답으로 동일한 작업을 수행할 수 있으므로 클라이언트가 HTTP POST를 수행한 다음 @PostMapping 메서드가 메시지 브로커에 메시지를 전송하여 가입한 클라이언트에게 브로드캐스트 할 수 있다.
    • 생성한 프로젝트에 예제를 추가해 보자.

    WebSocketConfig

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/portfolio")
                    .setAllowedOrigins("*");
        }
    
        @Override
        public void configureMessageBroker(MessageBrokerRegistry config) {
            config.setApplicationDestinationPrefixes("/app");
            config.enableSimpleBroker("/topic", "/queue");
        }
    }

     

    GreetingController

    @Controller
    public class GreetingController {
    
        @MessageMapping("/greeting")
        public String greeting(String message) {
            return "[" + System.currentTimeMillis() + ": " + message + "]";
        }
        
    }

     

    순서

    1. 클라이언트가 localhost:8080/portfolio에 연결하고 웹소켓 연결이 설정되면 STOMP 프레임으로 연결된다.
    2. 클라이언트는 대상 헤더가 /topic/gretting인 구독 프레임을 보낸다. 수신 및 디코딩이 완료되면 메시지는 clientInboundChannel로 전송된 후 클라이언트 구독을 저장하는 메시지 브로커로 라우팅 된다.
    3. 클라이언트는 /app/gretting으로 SEND 프레임을 보낸다. app 접두사는 주석이 달린 컨트롤러로 라우팅 하는 데 도움이 된다. app 접두사가 제거된 후 대상의 나머지 /greeting 부분은 GreetingController의 @MessageMapping 메서드에 매핑된다.
    4. GreetingController에서 반환된 값은 반환 값에 기반한 페이로드와 기본 대상 헤더인 /topic/gretting(입력 대상에서 /app이 /topic으로 대체되어 파생됨)을 포함하는 Spring Message로 변환된다. 결과 메시지는 brokerChannel로 전송되어 메시지 브로커에 의해 처리된다.
    5. 메시지 브로커는 일치하는 모든 구독자를 찾아 clientOutbountChannel을 통해 각 가입자에게 MESSAGE 프레임을 보내고, 여기서 메시지가 STOMP 프레임으로 인코딩 되어 웹소켓 연결로 전송된다.

     

    Test

    https://apic.app/online/#/tester

    위의 URL을 들어가게 되면 아래와 같이 테스트를 할 수 있다.

     

    맨 위쪽에 ws://localhost:8080/portfolio를 입력하고, Subscription URL에 구독하고자 하는 라우팅을 입력한다. /toplic/greeting을 입력하고 Connect 버튼을 누르면 해당 URL을 구독할 수 있다. 그런 다음 보내고자 하는 Destination Queue에 /app/greeting을 입력하고 텍스트를 입력한다. 그리고 Send 버튼을 누르면 구독 URL을 구독한 Subscirber에게 메시지를 전달하게 된다.

     

    Next Step

    다음은 외부 브로커를 이용하지 않고 채팅 애플리케이션을 개발해 보자.

    참고

    https://stomp.github.io/stomp-specification-1.2.html

     

    https://stomp.github.io/stomp-specification-1.2.html

    STOMP Protocol Specification, Version 1.2 Abstract STOMP is a simple interoperable protocol designed for asynchronous message passing between clients via mediating servers. It defines a text based wire-format for messages passed between these clients and s

    stomp.github.io

     

    반응형

    '백엔드' 카테고리의 다른 글

    Websocket? Socket? HTTP?  (0) 2023.10.05