반응형

지난 글(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를 사용하면서 메시지 브로커를 통한 중앙 집중식 구조를 가질 수 있게 되었다.

 

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

+ Recent posts