091
[JAVA/Spring Boot] STOMP: WebSocket 개념 본문
1. Spring과 WebSocket
- Spring에서는 모든 요청에 DispatcherServlet이라는 Front Controller에 들어오며, 이때 들어온 HTTP 요청 헤더에 Upgrade: websocket 존재 여부를 판단합니다. 일반적인 REST API 라우팅이 아닌 WebSocket 전용 처리기인 WebSocketHttpRequestHandler로 요청을 위임하여 3-way handshake 및 101 Switching Protocols 응답을 수헹힙니다.
(1) 순수 WebSocket:
- 스프링에서 제공하는 WebSocketHandler와 WebSocketSession 만을 이용하여 실시간 통신 서버를 구축하는 순수 WebSocket 방식은 생산성 저하나 구조적 결함을 야기하는 방식입니다.
-> WebSocketSession은 브라우저 쿠키 기반의 HttpSession과는 무관하며, 클라이언트와 서버 간에 연결된 물리적인 TCP 소켓의 파일 디스크립터 객체로 랭핑한 것입니다. 디스크립터(Descriptor)란 넘어온 메세지가 무슨 의미인지 설명하는 정보로, 넘어온 정보가 { "action": "hold", ... } 라고 할때 action="hold"의 역할을 말합니다. 하지만, 이건 문자열로 WebSocket이 이걸 모르기 때문에 아래처럼 코드를 작성해야합니다.
public class MyWebSocketHandler extends TextWebSocketHandler {
@Override
public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
ObjectMapper mapper = new ObjectMapper();
JsonNode json = mapper.readTree(payload);
String action = json.get("action").asText();
if ("hold".equals(action)) {
// 좌석 처리 로직
} else if ("chat".equals(action)) {
// 채팅 처리 로직
} else if ("payment".equals(action)) {
// 결제 처리 로직
}
}
}
-> 이 코드의 경우, 모든 로직이 한 곳에 몰려서 SPR(단일 책임 원칙)이 위배되었고, 타입 안정성이 없으며, URL 기반 라우팅이 아니기 때문에 저수준으로 이런 식으로 사용되지않습니다. 그래서 라우팅을 프로토콜 레벨에서 해결하자는 아이디어로 구현된 프로토콜이 STOMP 입니다.
*SPR(Single Responsibility Principle): 클래스는 하나의 책임만을 가져야하는 규칙입니다.
*String 기반 JSON 파싱은 타입 안정성이 없고 DTO 기반은 타입안정성이 있기 때문에 @Payload를 통해 명시해주기도 합니다.
2. STOMP(Simple Text Oriented Messaging Protocol)
- STOMP은 위와 같은 한계를 극복하기 위해 도입된 것으로, 순수 WebSocket이 제공하지 않던 종류와 목적 URI에 대한 규약입니다. 단순한 텍스트 지향 메시징 프로토콜로, 웹소켓 파이프(TCP) 위에서 동작하며 주고받는 데이터가 HTTP와 매우 유사한 프레임(Frame) 규격을 강제하는 서브 프로토콜입니다. 발행(Publish)-구독(Subscribe) 모델을 기반으로 작동합니다.
- 구조: HTTP 요청 메세지와 매우 비슷한 구조를 가지며, 크게 명령어,헤더,빈줄, 바디 4가지 요소로 구성됩니다.
COMMAND (명령어)
header-name:header-value (헤더)
header-name:header-value
Body (본문 데이터)
^@ (NULL 옥텟 - 프레임의 끝)
- STOMP 핵심 파이프라인: Channel은 메세지가 지나가는 내부 파이프입니다.
Client
↓
WebSocket
↓
Inbound Channel //Client->Server
↓
@MessageMapping (Controller)
↓
Outbound Channel //Server->Client
↓
Message Broker
↓
Client
(1) Client->WebSocket: 클라이언트와 서버간의 HTTP 핸드셰이크가 완료되고, 프로토콜이 웹소켓으로 업그레이드 된 상태입니다. 확린된 TCP 소켓을 통해 STOMP 규격에 맞춘 텍스트 프레임(위의 구조를 따른 JSON Payload)을 서버로 전송합니다.
(2) WebSocket->Inbound Channel: 웹소켓 버퍼에서 읽어 들인 STOMP 프레임이 스프링 메시징 시트엠의 내부통로인 clientInboundChannel로 유입되며, 스프링 내부에서 범용적으로 다루는 Message<byte[]> 객체로 디코딩됩니다.
-> 이 채널은 단순한 큐가 아닙니다. 수많은 클라이언트가 동시에 메세지를 쏟아낼 때 네트워크 스레드가 블로킹 되는 것을 막기 위해, 스프링은 이 채널 뒤에 별도의 스레드 풀을 구성하여 수신된 메세지들을 비동기적으로 병렬처리합니다.
(3) Inbound Channel->@MessageMapping(Controller): SimpAnnotationMethodMessageHandler라는 스프링 내부 컨포넌트가 clientInboundChannel을 구독하고 있다가 메세지를 낚아챕니다. 이 낚아챈 메세지의 헤더 속 destination을 분석하여, 개발자가 작성한 Controller의 @MessageMapping 메서드로 정확하게 라우팅합니다.
-> 네트워크 선을 타고 넘어오는 데이터는 바이트 배열인 JSON로만 전송이 가능한데, 위에서 설명했듯이 타입안정성을 위해 DTO를 파라미터로 요구하게 됩니다. 이때 스프링의 내부에 있는 Jackson 라이브러리를 활용한 MessageConverter를 활용하여 STOMP 프레임의 바디를 읽어들어 선언된 DTO와 JSON의 키값을 1:1로 매핑하여 파싱 코드를 작성하지 않게 만들어줍니다. 이렇게 문자열/바이트 데이터를 분석하여 자바 객체로 조립해내는 과정을 역직렬화(Deserialization)라고 합니다.
(4) @MessageMapping(Controller)->Outbound Channel: Controller에서 비지니스 로직 처리가 완료되고, 반환 값(@SendTo를 붙임)을 내보내거나 SimpMessagingTemplate을 사용하여 메시지를 수동으로 발송하며, 이 메세지는 brokerChannel이라는 내부 통로릍 탑승합니다.
->서버 로직은 이 데이터를 어느 유저의 소켓으로 직접 쏴야하는지에 대하 관리를 하지 않고, 이 데이터를 특정 목적이 라벨을 붙여 브로커 채널로 던집니다.
(5) Outbound Channel->Message Broker: brokerChannel을 통해 전달될 메시지는 스프링 내부의 인메모리 브로커 또는 연동된 외부 메시지 브로커에 도달합니다. 메세지 브로커는 자신이 메모리에 관리하고 있는 구독 맵을 탐색하여 전달받은 메세지의 목적지를 구독하고 있는 WebSocketSession의 고유 식별자 목록을 추출해냅니다.
-> 메세지 브로커는 발행/구독 패러다임을 실제로 구현하고 통제하는 미들웨어입니다. 메세지 브로커는 어떤 클라이언트가 어떤 목적지를 구독하고 있는지에 대한 거대한 매핑 테이블을 상시 유지하고 있고, 전달받은 메세지의 목적지를 확인하여 해당 목적지를 구독하는 클라이언트 세션에 메세지를 복제하여 흘려보내는 브로드캐스팅을 수행합니다. Spring에서는 JVM 메모리에서 동작하는 내장 브로커(SimpleBroker)를 기본적으로 사용하지만, 서버를 여러대로 다중화할 경우에는 서로의 존재를 알지 못해 메시지 유실이 발생합니다. 이때 스프링의 설정을 변경하는 외부 전용 메세지 브로커(RabbitMQ, ActiveMQ)를 사용하기도 합니다.
(6) Message Broker->Client: 브로커는 각각의 구독 세션들을 향해 메세지를 복제하여 전송하고 이 때 메세지가 최종적으로 서버를 빠져나가기 위한 채널인 clientOutboundChannel을 거칩니다. 이 채널을 통과하면서 내부 Message 객체 형태였던 데이터는 다시 STOMP 프레임 규격의 텍스트 스트림으로 직렬화됩니다.
=> 통신망에서는 문자열/바이트(JSON)으로만 돌아다니며, 스프링 내부의 비지니스 로직에서는 자바 객체(DTO)만 돌아다니도록 MessageConverter와 Message Wrapper가 끊임없이 형태를 바꿉니다.
'Programming Language > Java' 카테고리의 다른 글
| [KINO][JAVA/Spring Boot] 좌석 선정 및 예약: WebSocket 응용(2) (0) | 2026.03.28 |
|---|---|
| [KINO][JAVA/Spring Boot] 좌석 선정 및 예약: WebSocket 응용(1) (0) | 2026.03.24 |
| [JAVA/Spring Boot] Spring Framework: IoC, Bean, DI (0) | 2026.03.16 |
| [JAVA/Spring Boot] Spring MVC (0) | 2026.03.16 |
| [안드로이드 앱 개발] 뷰(View)(3) - ViewGroup(레이아웃) (1) | 2025.10.20 |