반응형

지난 글(WebSocket)에 이어서 STOMP에 대해 알아보자.

 

 

1. STOMP


STOMP(Simple/Streaming Text Oriented Messaging Protocol)란 클라이언트와 서버 간의 메시지 전송을 위한 텍스트 기반 프로토콜이다.

 

이 프로토콜은 Publish-Subscribe 구조를 기반으로 하며, Spring의 내장 브로커인 SimpleBroker나 외부 메시지 브로커(RabbitMQ, Redis, Kafka 등)를 활용하여 발신자(Publisher)와 수신자(Subscriber) 간의 메시지를 쉽게 주고받을 수 있다.

 

STOMP는 메시징 프로토콜이며, 발행자(Publisher)가 메시지를 생성하면 브로커가 이를 관리하고 구독자(Subscriber)에게 전달한다.

 

 

 

2. STOMP 구조


이전 글에서 WebSocket을 알아보며, 메세지 형식에 대한 규격이 없어 개발자가 직접 정의해야 하는 문제점들을 언급했다. STOMP를 사용하면 형식을 따로 고민할 필요도, 파싱하기 위한 코드를 구현할 필요도 없다.

 

STOMP는 프레임(frame)이라고 해서 Command, header, body로 이미 형식을 정의해 두었다.

frame

COMMAND
header1:value1
header2:value2

Body
  • Command는 메세지 유형을 나타내며 CONNECT, SEND, SUBSCRIBE 등이 있다.
  • Header은 메시지에 대한 부가 정보를 key-value 형태로 제공한다. 대표적으로 'destination' 헤더는 메시지의 목적지를 지정한다.
  • Body에 실제 전송할 데이터 내용을 포함하게 된다.

 

좌: WebSocket, 우: STOMP

 

 

 

3. STOMP 통신 흐름


STOMP 통신 흐름은 발신자, 서버, 브로커, 구독자 간의 상호작용을 통해 이루어진다.

 

STOMP 프로토콜에서 /topic/app 접두사는 다음과 같은 의미를 가진다.

  1. /topic : 이 접두사를 사용하면 메시지가 직접 메시지 브로커로 전달되어 해당 토픽을 구독하고 있는 모든 클라이언트에게 즉시 발행된다.
  2. /app : 이 접두사는 애플리케이션의 @MessageMapping 메서드로 메시지를 라우팅한다. 이 메서드에서 메시지를 처리한 후, 개발자가 명시적으로 SimpMessagingTemplate을 사용하여 /topic으로 메시지를 전송해야 구독자들에게 발행이 된다.

 

따라서, /app으로 들어온 메시지는 서버에서 추가 처리(데이터베이스 저장, 메시지 변환, 인증 등)를 할 수 있는 기회를 제공하며, 이 처리 후에 개발자가 명시적으로 /topic으로 메시지를 전송해야 구독자들에게 발행된다.

 

이러한 구조는 단순히 메시지를 전달하는 경우와 서버에서 추가 로직이 필요한 경우를 유연하게 처리할 수 있게 해준다.

 

 

 

4. Spring STOMP


이전에 WebSocket을 구현해 볼 때 사용했던 WebSocket King Client의 경우 Command, header, body로 이루어진 STOMP의 프레임 타입을 지원하지 않는다. 따라서 STOMP 테스트를 만들어준 블로그를 이용하여 구현해 보자.

 

 

4.1 환경설정

WebSocket과 STOMP를 Gradle에 의존성 추가

gradle

implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.webjars:stomp-websocket:2.3.3'

 

 

4.2 Config

java

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        // 클라이언트가 구독할 prefix (예: /topic)
        registry.enableSimpleBroker("/topic");
        // 클라이언트가 메시지를 보낼 때 사용할 prefix (예: /app)
        registry.setApplicationDestinationPrefixes("/app");

    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOriginPatterns("*");
    }
}

STOMP를 사용하면 더 이상 SocketTextHandler와 같은 저수준 WebSocket 핸들러를 직접 구현할 필요가 없다.

 

STOMP는 메시지 라우팅, 구독 관리, 메시지 형식 등을 추상화하여 제공하므로, 개발자는 비즈니스 로직에 더 집중할 수 있다.

 

대신, @MessageMapping 어노테이션을 사용하여 특정 목적지로 오는 메시지를 처리하는 메서드를 정의하게 된다.

WebSocket 때와 마찬가지로 Tester에서도 .withSockJs() 를 통해 SockJs를 활성화할 수 있지만 오류가 날 수 있으니 제거하고 실습하자.

 

 

4.3 Controller

java

@Controller
@RequiredArgsConstructor
public class ChatController {

    private final SimpMessagingTemplate messagingTemplate;

    @MessageMapping("/chat/enter")
    public void enterChatRoom(@RequestBody ChatDto chatDto, SimpMessageHeaderAccessor headerAccessor) {
        Map<String, Object> sessionAttributes = headerAccessor.getSessionAttributes();

        sessionAttributes.put("id", chatDto.getId());
        sessionAttributes.put("roomCode", chatDto.getRoomCode());

        String joinMessage = chatDto.getId() + "님이 입장하셨습니다.";
        messagingTemplate.convertAndSend("/topic/chat/room/" + chatDto.getRoomCode(), joinMessage);
    }

    @MessageMapping("/chat/send")
    public void sendMessage(@RequestBody ChatDto chatDto) {
        String msg = chatDto.getId() + ": " + chatDto.getMessage();
        messagingTemplate.convertAndSend("/topic/chat/room/" + chatDto.getRoomCode(), msg);
    }
}

@MessageMapping 어노테이션이 붙은 메서드는 WebSocketConfig에서 설정한 applicationDestinationPrefixes ("/app")와 결합된다. 따라서 클라이언트에서 "/app/chat/enter"로 보낸 메시지가 이 메서드로 라우팅 된다.

 

enterChatRoom 메서드는 사용자의 채팅방 입장을 처리한다. 세션 속성에 사용자 ID와 roomCode를 저장하고 입장 메시지를 생성한다. 그 후, messagingTemplate을 사용하여 "/topic/chat/room/{roomCode}" 목적지로 메시지를 전송하며 해당 채팅방을 구독 중인 모든 클라이언트에게 전달된다.

 

sendMessage 메서드에서 메세지를 가공하거나 DB에 저장하는 로직을 추가할 수 있다. DB 연결은 생략하고 메세지만 다음과 같이 가공하여 보내보자. String msg = chatDto.getId() + ": " + chatDto.getMessage();

 

 

4.4 Dto

java

@Data
public class ChatDto {
    private String id;
    private String roomCode;
    private String message;
}

DB가 없으니 Entity는 생략하고 Dto만 간단히 만들어보자.

 

 

4.5 SpringBoot + STOMP TESTER

먼저 STOMP TESTER 에 접속 후 창을 2개 켜서 같은 채팅방에 구독하고, 입장 해보자

 

DESTINATION PATH: /app/chat/enter

MESSAGE(JSON): {"id": "User1", "roomCode": "ROOM123"}

위와 같이 /topic/chat/room/ROOM123으로 구독한 후

 

유저의 정보와 채팅방 정보를 담은 메세지를 보내게 되면 채팅방 입장 메세지가 구독한 유저에게 모두 보이게 된다.

이후로는 아래와 같이 JSON 객체에 message 변수를 추가하여 메세지를 보낼 수 있다.

 

DESTINATION PATH: /app/chat/send

MESSAGE(JSON): {"id": "User1", "roomCode": "ROOM123", "message":"Hello"}

 

 

 

마치며,

 

WebSocket이 단순히 클라이언트-서버 간 1:1 통신을 제공하는 반면, STOMP를 사용하면서 메시지 브로커를 통한 중앙 집중식 구조를 가질 수 있게 되었다.

 

이로 인해 메시지의 관리와 분배가 더 체계적으로 이루어질 수 있으며, 개발자는 복잡한 메시징 로직 구현에 집중하기보다는 비즈니스 로직에 더 집중할 수 있다.

반응형

1. Socket


소켓네트워크에서 데이터를 주고받을 수 있도록 네트워크 환경에 연결할 수 있게 만들어진 연결부이다.

 

마치 전화기와 비슷하다. 두 사람이 전화로 대화하려면 각자 전화기가 필요하듯이, 두 컴퓨터 프로그램이 대화하려면 각각 소켓이 필요하다. 따라서 클라이언트 소켓, 서버 소켓으로 구분된다.

 

현재 대부분의 네트워크 통신은 인터넷 프로토콜을 사용하며, 대표적으로 TCP/IP, UDP/IP가 존재한다. TCP와 UDP는 소켓을 통해 구현되는 가장 흔한 프로토콜이다.

 

 

 

2. WebSocket


웹 소켓하나의 TCP 접속에 전이중 통신 채널을 제공하는 컴퓨터 통신 프로토콜로, 소켓 통신에 기반하여 웹 애플리케이션에 맞게 발전한 형태이다. HTTP나 HTTPS 위에서 동작하도록 설계되었으며, 서버와 브라우저 간 연결을 유지한 상태로 추가적인 HTTP 요청 없이 데이터를 교환할 수 있다.

 

 

2.1 HTTP와 WebSocket의 차이

Http와 WebSocket Protocol의 가장 큰 차이는 수립된 커넥션을 어떻게 하느냐이다.

 

Http는 비 연결성 프로토콜로 클라이언트가 요청을 보낼 때마다 연결을 맺고 응답을 받은 후 연결을 끊어버린다.

웹 소켓 이전의 HTTP 기반으로 한 실시간 통신 방식으로 폴링(Polling)롱 폴링(Long Polling)이 있지만, 클라이언트가 요청을 지속적으로 보내야 하는 상황을 벗어나지 못했다.

 

반면에 웹 소켓의 경우 한번 연결을 맺고 나면 어느 한쪽에서 연결을 끊으라는 요청을 보내기 전까지 연결을 유지하기 때문에 매번 연결할 때마다 발생하는 비용을 줄일 수 있다.

 

다시한번 정리하면 HTTP는 요청과 응답이 한 쌍을 이루는 구조로 통신을 한다. 즉, 내가 원하는 어떤 것을 얻기 위해서는 항상 그것을 달라고 요청을 해야했다. 반면, 웹 소켓은 연결이 계속 유지되고 있는 상태이기 때문에, 연결된 채널, 소켓을 통해 상대가 보내오는 메세지를 듣기만 하면 된다.

 

그렇다면 WebSocket은 어떻게 연결되는 것일까?

 

 

2.2 핸드셰이크(handShake)

웹 소켓 커넥션을 만들려면 new WebSocket() 을 호출하면 되며 이때 handShake라는 과정을 HTTP 프로토콜을 사용해 시작한다.

 

핸드 셰이크가 성공적으로 완료되면, 연결은 HTTP에서 WebSocket Protocol로 전환되며 HTTP → ws://, HTTPS → wss:// 로 변경된다.

 

 

 

3. SpringBoot + WebSocket King Client


Chrome 확장 프로그램인 WebSocket King Client에서 WebSocket을 구현해보자.

 

 

3.1 환경설정

  • Chrome에서 WebSocket King Client 설치 후 접속
  • Spring Boot 2.7.18
  • Java 11

 

websocket과 JSON을 사용하기 위해 Gradle에 의존성 추가

gradle

    implementation 'org.springframework.boot:spring-boot-starter-websocket'
    implementation 'org.json:json:20211205'

 

 

3.2 Config

Spring에서 웹 소켓을 사용하려면 클라이언트가 보내오는 통신을 처리할 Handler가 필요하다.

java

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketTextHandler(), "/ws")
                .setAllowedOriginPatterns("*");
    }
}

이후 구현할 Handler를 웹 소켓이 연결될 때 handShake할 주소와 함께 인자로 넣어주면 된다.

CORS 제어를 위해 setAllowedOriginPatterns를 설정하여 특정 도메인에서만 WebSocket 연결을 허용할 수 있다.

.withSockJs() 를 통해 SockJs를 활성화 할 수 있지만 WebSocket Client King에서 오류가 날 수 있으니 제거하고 실습하자.

 

 

3.3 Handler

java

public class SocketTextHandler extends TextWebSocketHandler{

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

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("user: " + session.getId() + " connected"));
        }
    }

    @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(jsonObject.get("user") + ": " +  jsonObject.get("message")));
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        sessions.remove(session);
        for (WebSocketSession s : sessions) {
            s.sendMessage(new TextMessage("user: " + session.getId() + " disconnected"));
        }
    }
}

 

웹 소켓은 기본적으로 Tex t또는 Binary 타입을 지원한다.

필요에 따라 TextWebSocketHandler 또는 BinaryWebSocketHandler라는 Spring이 기본적으로 제공하는 클래스를 상속하고 구현하면 된다.

 

이때, 메서드 인자로 받아오는 WebSocketSession이 있다. 흔히 말하는 HTTP 세션과는 다른 맥락으로 간단히 말하면 웹 소켓이 연결될 때 생기는 연결정보를 담고 있는 객체라고 보면 된다.

 

핸들러에서 웹 소켓 통신에 대한 처리를 하기 위해 이 세션들을 컬렉션으로 담아 관리하는 경우가 많으며, 위 코드를 보면 커넥션이 맺어질 때 컬렉션에서 웹 소켓 세션을 추가하고 끊어질 때 제거를 하고 있다.

 

추가로 세션이 연결됨과 끊어질 때, 입장과 퇴장 메세지를 보낼 수 있게 설정해놓았다.

 

handleTextMessage는 WebSocket을 통해 텍스트 메세지를 받았을 때 실행되는 핸들러로 받은 메세의 내용(payload, 클라이언트에서 JSON으로 보냈다고 가정)을 각 연결된 세션에 새 메세지를 보내게된다.

 

 

3.4 WebSocket King Client

상단에 CONNECTIONS을 하나 더 늘려 유저 A와 B가 각각 입장하여 채팅을 한다고 테스트해보자.

 

ws://localhost:8080/ws 주소로 연결과 함께 자동으로 부여된 session Id가 출력됨을 확인할 수 있다. /ws는 config에서 설정한 end-point이다.

 

이후 채팅을 JSON 형태로 보내게 되면, Handler에서 ‘user’‘message’ 의 key 값으로 메세지를 처리하는 걸 확인할 수 있다.

 

개발자 도구를 살펴보면 처음 handShake 과정에서 HTTP를 사용하기 때문에 유사한 양의 헤더 정보를 주고받게 되지만 한번 연결이 수립되고 나서는 간단한 Messages들만 오고 가는 것을 확인할 수 있다.

 

 

  • Origin : 클라이언트 origin을 나타낸다.
  • Connection : Upgrade, 클라이언트 측에서 프로토콜을 바꾸고 싶다는 신호를 보냈다는 것을 나타낸다. (http → ws)
  • Upgrade : websocket, 클라이언트 측에서 요청한 프로토콜은 'websocket’이라는 것을 의미한다.
  • Sec-WebSocket-Key : 보안을 위해 브라우저에서 생성한 키로, 서버가 웹 소켓 프로토콜을 지원하는지를 확인하는 데 사용된다.

 

마치며

WebSocket은 실시간 양방향 통신을 제공하는 강력한 기술이지만 몇 가지 한계가 있다.

 

1️⃣ 메세지 구조화

WebSocket은 단순한 텍스트 교환만을 제공하며 메세지 형식에 대한 규격이 없다. 우리는 handler에서 String 값인 payload를 JSON으로 파싱하여 사용하였다. 따라서 복잡한 메시지 패턴이나 로직들을 개발자가 직접 정의해야하고 구현해야 한다.

 

2️⃣ 클라이언트 관리

세션의 ID는 서버가 클라이언트와의 WebSocket 연결될 때 자동으로 생성되는 랜덤한 값이기 때문에 연결된 클라이언트들을 추적하고 관리하는 시스템을 직접 구축해야한다.

 

3️⃣ 메세지 라우팅

특정 주제(Topic)나 채널(Channel)에 따라 메시지를 구분하거나 라우팅하는 기능이 내장되어 있지 않기 때문에, 이를 구현하려면 추가적인 로직과 구조를 설계해야한다.

 

 

 

이러한 어려움을 해결하기 위해 STOMP와 같은 상위 프로토콜을 사용하면 메지 라우팅과 세션 관리를 더 쉽게 구현할 수 있다.

 

다음 글에서는 STOMP에 대해 알아보고 실습해 보는 시간을 가져보자.

 

 

[Spring] STOMP란? (Spring Boot + STOMP TESTER로 구현해보기)

지난 글(WebSocket)에 이어서 STOMP에 대해 알아보자.  1. STOMPSTOMP(Simple/Streaming Text Oriented Messaging Protocol)란 클라이언트와 서버 간의 메시지 전송을 위한 텍스트 기반 프로토콜이다. 이 프로토콜은 Pu

murphytklee.tistory.com

 

반응형
반응형

+ Recent posts