์ค๋์ ๊ทธ๋ฅ ๊ฐ๋ณ๊ฒ WebSocket์ด๋ STOMP๊ฐ ๋ญ์ง ํ ๋ฒ ์ ๋ฆฌํด๋ณด๋ ค๊ณ ํ๋ค.
ํ๋ก์ ํธ ํ๋ค ๋ณด๋ฉด ์ฑํ ์ด๋ ์๋ฆผ ๊ฐ์ ์ค์๊ฐ ๊ธฐ๋ฅ์ ๋ฃ์ด์ผ ํ ๋๊ฐ ์๋๋ฐ, ๊ทธ๋ด ๋๋ง๋ค ์ ํํ ๋์ ์ญํ ์ด ๋ญ์๋์ง ์ด์ง ํท๊ฐ๋ฆด ๋๊ฐ ์๋๋ผ. ๊ทธ๋์ ์ด๋ฒ์ ํ ๋ฒ ์ ๋๋ก ์ ๋ฆฌํด๋๋ฉด ๋์ค์ ๋ค์ ๋ณผ ๋ ํธํ ๊ฒ ๊ฐ์์ ์ ์ด๋ณธ๋ค.
์ฐ๋ฆฌ๊ฐ ํํ ์๊ณ ์๋ HTTP๋ Stateless, Connectionless ํ๋ค๋ ํน์ง์ ๊ฐ์ง๊ณ ์๋ค.
์ฆ, ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์์ฒญ์ ํ ๋ฒ ๋ณด๋ด๊ณ ์๋ต์ ๋ฐ์ผ๋ฉด ๊ทธ ์ฐ๊ฒฐ์ ๋ฐ๋ก ๋๊ธฐ๊ณ , ์๋ฒ๋ ๋ฐฉ๊ธ ๋๊ตฌ์๋์ง์ ๋ํ ์ํ๋ ๊ธฐ์ตํ์ง ๋ชปํ๋ค.
๋ฑ ์์ฒญโ์๋ต ํ ๋ฒ ํ๊ณ ๋๋๋, ์ ๋ง ๋จ๋ฐ์ฑ ํต์ ๋ฐฉ์์ธ ๊ฑฐ๋ค.
๊ทธ๋์ ์ด๋ฐ ๊ตฌ์กฐ๋ ์ผ๋ฐ์ ์ธ ์น ํ์ด์ง ์์ฒญ์ฒ๋ผ โํ์ํ ๋๋ง ์ ๊น ๋ฌผ์ด๋ณด๊ณ ๋ฐ๋โ ์์ ์๋ ์ ๋ง์ง๋ง, ์ฑํ ์ฒ๋ผ ๊ณ์ ๋ํ๋ฅผ ์ด์ด๊ฐ์ผ ํ๊ฑฐ๋ ์ค์๊ฐ์ผ๋ก ๋ญ๊ฐ๋ฅผ ํธ์ํด์ผ ํ๋ ์ํฉ์์๋ ํ๊ณ๊ฐ ์๋ค.
๋ฐ๋ก ์ด ์ง์ ์ด WebSocket์ด ๋ฑ์ฅํ๊ฒ ๋ ์ด์ ๋ค.
WebSocket์ HTTP์๋ ๋ค๋ฅธ ํ๋กํ ์ฝ์ด์ง๋ง, ์ต์ด ์ฐ๊ฒฐ ์ HTTP Upgrade ์์ฒญ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ 80/443 ํฌํธ๋ฅผ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์๊ณ ๊ธฐ์กด ๋ฐฉํ๋ฒฝ ๊ท์น๋ ์ฌ์ฌ์ฉํ ์ ์๋๋ก ์ค๊ณ๋์๋ค.
๋ฐ์ HTTP ์์ฒญ ์์๋ฅผ ๋ณด์
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket --- 1
Connection: Upgrade --- 2
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
Upgrade ํค๋๋ฅผ ํ์ฑํํ๋ ์ฉ๋๊ทธ๋ฌ๋ฉด ์๋ฒ๋ ์๋ต์ ์ด๋ ๊ฒ ํ๋ค.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
101 Switching Protocols
์๋ฒ๊ฐ โ์ข์, WebSocket ํ๋กํ ์ฝ๋ก ์ ํํ ๊ฒโ๋ผ๊ณ ์น์ธํ ๊ฒ. ํธ๋์
ฐ์ดํฌ๊ฐ ์ฑ๊ณตํ๋ค๋ ์๋ฏธ
Upgrade: websocket
์ด์ ๋ถํฐ WebSocket ํ๋กํ ์ฝ๋ก ํต์ ํ ๊ฑฐ๋ผ๋ ์ ์ธ
Connection: Upgrade
Upgrade ํค๋๋ฅผ ํ์ฑํํ๋ ์ญํ

WebSocket์ ํฌ๊ฒ 3๋จ๊ณ๋ก ๋์ํ๋ค
๋จผ์ ๋ธ๋ผ์ฐ์ ๊ฐ ์๋ฒ์๊ฒ ์ด๋ ๊ฒ ๋ฌป๋๋ค:
๐ฃ๏ธ โ์ฐ๋ฆฌ HTTP ๋ง๊ณ WebSocket์ผ๋ก ์๊ธฐํ ๋? Protocol Upgrade ํ ๊ฒ!โ
์ด๋ ๋ธ๋ผ์ฐ์ ๋ HTTP/1.1 GET ์์ฒญ + Upgrade ํค๋๋ฅผ ๋ณด๋ธ๋ค.
์๋ฒ๋ ์์ฒญ์ ๋ณด๊ณ OK๋ผ๋ฉด
๐ฃ๏ธ โ์ข์, ์ง๊ธ๋ถํฐ WebSocket ํ๋กํ ์ฝ๋ก ์ ํํ๋ค(101 Status)โ
๋ผ๋ ์๋ต์ ๋ณด๋ธ๋ค.
์ฌ๊ธฐ๊น์ง๋ ์์ ํ HTTP ํต์ ์ด๋ค.
์๋ต์ด ๋๋๋ ์๊ฐ๋ถํฐ๋ HTTP๊ฐ ๋๊ธฐ๊ณ WebSocket ํ๋กํ ์ฝ์ด ๋๋ค.
Handshake๊ฐ ๋๋๋ฉด ์ด์ ์ง์ง WebSocket์ด ์์๋๋ค.
์ด์ ๋ถํฐ๋ HTTP ๋ฉ์์ง๊ฐ ์๋๋ผ ํ๋ ์(Frame) ์ ์ฃผ๊ณ ๋ฐ๋๋ค.
์์ชฝ ๋ชจ๋ ์์ฒญ/์๋ต ๊ด๊ณ ์์ด ์์ ๋กญ๊ฒ ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ์ ์๋ค.
์ฆ, WebSocket์ ํต์ฌ ๊ธฐ๋ฅ์ธ ์ค์๊ฐ ์๋ฐฉํฅ ํต์ ๋จ๊ณ
์ฃผ๊ณ ๋ฐ๋ ๋ฐ์ดํฐ๋ ๋ชจ๋ WebSocket ํ๋ ์ ํ์์ด๋ฉฐ, ํ ์คํธ ํ๋ ์ ๋๋ ๋ฐ์ด๋๋ฆฌ ํ๋ ์์ผ๋ก ์ ์ก๋๋ค.
ํต์ ์ค ์ด๋ ์ชฝ์ด๋ โ๊ทธ๋งํ์โ๋ผ๊ณ Close ํ๋ ์์ ๋ณด๋ผ ์ ์๋ค.
์: ๋ธ๋ผ์ฐ์ ๊ฐ โ๋๋ผ๊ฒ์(Close Frame)โ
์๋ฒ๊ฐ โ์ค์ผ์ด(Close Response Frame)โ ํ๊ณ ์๋ต
๋๋ ์๋ฒ๊ฐ ๋จผ์ ์ข ๋ฃ ์์ฒญํ ์๋ ์๋ค.
Close ํ๋ ์์ด ์ค๊ฐ๋ฉด ์ฐ๊ฒฐ์ ์ ์์ ์ผ๋ก ๋ซํ๊ณ , WebSocket ์ฐ๊ฒฐ์ด ์ข
๋ฃ๋๋ค.
๋ฌผ๋ก ์์์ฒ๋ผ WebSocket ์ฐ๊ฒฐ์ด ์๋๋ฉด ์ ๋ง ์ต๊ณ ๋ค. ํ์ง๋ง ์ธ์์ฒ๋ผ, ํ์ค์์๋ ๋ ๊ณํ๋๋ก ๋์ง๋ ์๋๋ค.
ํ์ฌ ๋คํธ์ํฌ๋ ํ๊ต ์์ดํ์ด, ํน์ ์ด๋ค ํ๋ก์ ์ฅ๋น๋ค์ ๊ฑฐ์น๋ฉด WebSocket ์์ฒด๋ฅผ ์์ ๋ง์๋์ ๊ฒฝ์ฐ๊ฐ ๊ฝค ๋ง๋ค.
๊ทธ๋์ ํด๋ผ์ด์ธํธ๋ โ๋ถ๋ช ๋ด ์ฝ๋๋ ๋ง๋๋ฐ ์ ์ฐ๊ฒฐ์ด ์ ๋์ง?โ ํ๋ฉด์ ์์ธ์ ์ ์ ์๋ ์๋ฌ๋ฅผ ๋ง๋๋ ์ผ์ด ์๊ธด๋ค.
์ด๋ด ๋ ๋ฑ์ฅํ๋ ๊ฒ SockJS๋ค.
SockJS๋ ํ๋ง๋๋ก ๋งํ๋ฉด,
WebSocket์ด ๋๋ฉด WebSocket์ ์ฐ๊ณ , ์ ๋๋ฉด ์กฐ์ฉํ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ๋์ด๊ฐ์ ์ต๋ํ WebSocket์ฒ๋ผ ๋์ํ๊ฒ ๋ง๋ค์ด์ฃผ๋ ์์ ์ฅ์น
๋ผ๊ณ ๋ณด๋ฉด ๋๋ค.
์น์์ผ์ด ์ฐ๊ฒฐ์ด ์ ๋ ๋, SockJS๋ ๋ฐ๋ก ํฌ๊ธฐํ์ง ์๊ณ
๋ค๋ฅธ ํด๋ฐฑ(fallback) ๋ฐฉ์๋ค์ ์์๋๋ก ์๋ํ๋ค.
์๋ฅผ ๋ค๋ฉด XHR Streaming, Long Polling ๊ฐ์ ๊ฒ๋ค์ด๋ค.
๋๋ถ์ ๊ฐ๋ฐ์๋ โWebSocket์ด ๋๋ ์ ๋๋โ ๊ฑฑ์ ํ์ง ์๊ณ ํญ์ ์๋ฐฉํฅ ํต์ ์ด ๋๋ ํต๋ก๋ฅผ ํ๋ณดํ ์ ์๋ค.
Spring์์ .withSockJS()๋ฅผ ๋ถ์ด๋ ์ด์ ๋ ์ด๊ฑฐ๋ค.
WebSocket์ด ์ ๋๋ ํ๊ฒฝ์์๋ ๊ทธ๋ฅ WebSocket๋ก ์ฐ๊ฒฐํ๊ณ , ๋ฌธ์ ๊ฐ ์๋ ํ๊ฒฝ์์๋ ์๋์ผ๋ก ์์ ํ ๋ค๋ฅธ ๋ฐฉ์์ผ๋ก ์ ํ๋๋ค.
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new MyWebSocketHandler(), "/ws")
.setAllowedOrigins("*") // CORS ํ์ฉ
.withSockJS();
}
}
์ ๋ฆฌํ์๋ฉด,
์ด ์ ๋ ๋๋์ผ๋ก ์ดํดํ๋ฉด ๋๋ค.
STOMP๋ WebSocket ์์์ ๋์ํ๋ ๋ฌธ์ ๊ธฐ๋ฐ ๋ฉ์์ง ํ๋กํ ์ฝ๋ก, ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ์ฃผ๊ณ ๋ฐ๋ ๋ฉ์์ง์ ์ ํ, ํ์, ๊ตฌ์กฐ๋ฅผ ๋ช
ํํ๊ฒ ์ ์ํด์ฃผ๋ ์ญํ ์ ํ๋ค.
WebSocket์ ๋จ์ํ ์๋ฐฉํฅ ํต์ ํต๋ก๋ฅผ ์ด์ด์ฃผ๋ ์์ค์ด์ง๋ง, STOMP๋ ๊ทธ ํต๋ก ์์์ ๋ฉ์์ง๊ฐ ์ด๋ป๊ฒ ์ค๊ฐ์ผ ํ๋์ง์ ๋ํ โ๊ท์นโ์ ์ ๊ณตํ๋ค๊ณ ๋ณด๋ฉด ๋๋ค.
์ด ํ๋กํ ์ฝ์ TCP, WebSocket์ฒ๋ผ ์ ๋ขฐํ ์ ์๋ ์๋ฐฉํฅ ์คํธ๋ฆฌ๋ฐ ๋คํธ์ํฌ ํ๋กํ ์ฝ ์์์ ์ฌ์ฉํ ์ ์๋๋ก ์ค๊ณ๋์ด ์๋ค. STOMP ์์ฒด๊ฐ ๋ฉ์์ง ์ ์ก ๊ท์ฝ๋ง ์ ์ํ ๋ฟ, ์ค์ ์ ์ก์ ํ์ ๋คํธ์ํฌ ํ๋กํ ์ฝ(WebSocket ๋ฑ)์ด ๋ด๋นํ๋ค.
๋ ํ๋ ์ค์ํ ํน์ง์ ๊ธฐ๋ณธ์ ์ผ๋ก pub/sub ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ๋ค๋ ์ ์ด๋ค. ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ ๊ณณ(SEND)๊ณผ ๋ฉ์์ง๋ฅผ ๊ตฌ๋
ํ๋ ๊ณณ(SUBSCRIBE)์ด ๋ช
ํํ๊ฒ ๋๋์ด ์์ด์, ์ด๋ค ๋ชฉ์ ์ง๋ก ๋ฉ์์ง๊ฐ ๋ฐํ๋๊ณ , ๋๊ฐ ๊ทธ ๋ชฉ์ ์ง๋ฅผ ๊ตฌ๋
ํ๊ณ ์์ผ๋ฉฐ, ์ด๋ค ๋ฐฉ์์ผ๋ก ๋ฉ์์ง๊ฐ ์ ๋ฌ๋๋์ง๊ฐ ํ์คํ๊ฒ ์ ํด์ ธ ์๋ค.
STOMP ์ญ์ HTTP์ฒ๋ผ ํ๋ ์(Frame) ๊ธฐ๋ฐ ํ๋กํ ์ฝ์ด๋ฉฐ, ๊ฐ ๋ฉ์์ง๋ ํค๋์ ๋ฐ๋๋ฅผ ๊ฐ๋ ํน์ ํ์์ ํ๋ ์์ผ๋ก ์ ์ก๋๋ค. ๋๋ถ์ ๋ฉ์์ง๋ ๋จ์ ๋ฌธ์์ด์ด ์๋๋ผ ๊ตฌ์กฐํ๋ ํํ๋ก ๊ตํ๋ ์ ์๋ค.
์์ฝํ์๋ฉด, STOMP๋ WebSocket ์์์ ๋ฉ์์ง ํ์๊ณผ ๋ผ์ฐํ ๊ท์น์ ์ ์ํด์ฃผ๋ ๊ณ ์์ค ํ๋กํ ์ฝ์ด๋ฉฐ, pub/sub ํจํด์ ๊ธฐ๋ฐ์ผ๋ก ์ค์๊ฐ ๋ฉ์์ง์ ๋ณด๋ค ์ฒด๊ณ์ ์ด๊ณ ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋๋ก ๋์์ฃผ๋ ์ญํ ์ ํ๋ค.
์ฌ์ค WebSocket๋ง ๋๊ณ ๋ณด๋ฉด ๊ธฐ๋ฅ์ด ๊ต์ฅํ ๋จ์ํ๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก ๋ฌธ์์ด์ ์ฃผ๊ณ ๋ฐ๋ ์ ๋์ ๋ถ๊ณผํ๊ณ , ๋ฉ์์ง๊ฐ ๋๊ตฌ์๊ฒ ์ ๋ฌ๋ผ์ผ ํ๋์ง, ์ด๋ค ๋ชฉ์ ์ง๋ก ๋ณด๋ด์ผ ํ๋์ง, ์ฑํ ๋ฐฉ ๊ฐ์ ๊ฐ๋ ์ ์ ํ ์๋ค. ์ฌ์ง์ด ๋ฉ์์ง ํ์ ์กฐ์ฐจ ๊ตฌ๋ถ๋์ง ์๋๋ค. ๋ง ๊ทธ๋๋ก โ์ด๋ฆฐ ์์ผ์์ ๋ฌธ์์ด์ ๋ณด๋ด๊ณ ๋ฐ๋๋คโ๊ฐ ๋์ธ ์ ์ด๋ค.
์ด๋ ๊ฒ ๋จ์ํ๋ค ๋ณด๋, ์ค์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ง๋ค ๋ ํ์ํ ๊ธฐ๋ฅ๋ค์ ์ ๋ถ ์ง์ ๊ตฌํํด์ผ ํ๋ค.
์๋ฅผ ๋ค๋ฉด,
์ด๋ฐ ๊ฒ๋ค์ ๋งค๋ฒ ์ฒ์๋ถํฐ ๋ง๋ค๋ ค๊ณ ํ๋ฉด ์ฌ์ค์ ๊ฐ๋ฐ ์ง์ฅ์ด ํผ์ณ์ง๋ค.
๊ทธ๋์ ๋ฑ์ฅํ ๊ฒ์ด ๋ฐ๋ก STOMP๋ค.
STOMP๋ WebSocket ์์์ โ๋ฉ์์ง๋ฅผ ์ด๋ป๊ฒ ๋ณด๋ด๊ณ ๋ฐ์์งโ์ ๋ํ ๊ท์น์ ์ ๊ณตํด์ฃผ๋ ํ๋กํ ์ฝ๋ก, WebSocket์ ๋จ์ํ ํต์ ๊ตฌ์กฐ์ ๊ตฌ๋ , ๋ฐํ, ๋ชฉ์ ์ง ๋ผ์ฐํ , ๋ฉ์์ง ๊ตฌ์กฐํ ๊ฐ์ ๊ณ ์์ค ๊ธฐ๋ฅ์ ์น์ด์ค๋ค.
๋๋ถ์ ๊ฐ๋ฐ์๋ ๋ณต์กํ ๋ฉ์์ง ๋ก์ง์ ์ง์ ๋ง๋ค ํ์ ์์ด, ์ ํด์ง ๊ท์ฝ์ ๋ฐ๋ผ ํจ์ฌ ๊น๋ํ๊ฒ ์ค์๊ฐ ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๊ฒ ๋๋ค.
STOMP๋ WebSocket ์์์ ๋์ํ๋ โ๋ฉ์์ง ๊ท์ฝ(ํ๋กํ ์ฝ)โ์ด๋ค. ์ฝ๊ฒ ๋งํ๋ฉด, โWebSocket์ ํต๋ก๋ง ์ด์ด์ค ๋ฟ์ด๊ณ , STOMP๋ ๊ทธ ํต๋ก ์์์ ๋ฉ์์ง๋ฅผ ์ด๋ค ๊ท์น์ผ๋ก ์ฃผ๊ณ ๋ฐ์์ง ์ ํด์ฃผ๋ ์ฝ์.โ
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// ์๋ฒ โ ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ prefix (subscribe)
registry.enableSimpleBroker("/topic", "/queue");
// ํด๋ผ์ด์ธํธ โ ์๋ฒ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ prefix (publish)
registry.setApplicationDestinationPrefixes("/app");
}
// ํด๋ผ์ด์ธํธ๊ฐ WebSocket ์ฐ๊ฒฐ์ ์์ฑํ๋ endpoint
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*") // CORS ํ์ฉ
.withSockJS(); // SockJS fallback ์ง์
}
}
@EnableWebSocketMessageBroker
STOMP ๊ธฐ๋ฐ์ ๋ฉ์์ง ๋ธ๋ก์ปค ๊ธฐ๋ฅ์ ์ผ์, WebSocket์์ publish/subscribe ๊ตฌ์กฐ๋ฅผ ์ฌ์ฉํ ์ ์๊ฒ ํด์ค๋ค.
registry.enableSimpleBroker("/topic", "/queue")
์ฆ, ํด๋ผ์ด์ธํธ๊ฐ subscribe(๊ตฌ๋
)ํ๋ ๊ฒฝ๋ก๋ฅผ ์ค์ ํ๋ ๋ถ๋ถ์ด๋ค. ์๋ฒ๋ ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ๋ ํด๋น prefix๋ก ์์ํ๋ ๋ชฉ์ ์ง๋ก ์ ๋ฌํ๋ค. /topic โ ์ฌ๋ฌ ํด๋ผ์ด์ธํธ์๊ฒ ๋์์ ๋ณด๋ด๋ ๋ธ๋ก๋์บ์คํธ(broadcast)
์) ์ฑํ
๋ฐฉ ๋ฉ์์ง, ๊ณต์ง
/queue โ ํน์ ์ฌ์ฉ์์๊ฒ๋ง ๋ณด๋ด๋ 1:1(point-to-point)
์) ์๋ฆผ, ๊ฐ์ธ ๋ฉ์์ง
registry.setApplicationDestinationPrefixes("/app")
์ฆ, publish ์์ฒญ์ ๋ณด๋ผ ๋ ์ฌ์ฉํ๋ prefix๋ฅผ ์ง์ ํ๋ค.
ํด๋ผ์ด์ธํธ๊ฐ /app/** ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ฉด Spring์ ์ด ์์ฒญ์ @MessageMapping ๋ฉ์๋๋ฅผ ์ฐพ์์ ์ ๋ฌํ๋ค.
@MessageMapping("/hello")
public void messageHandler(...) { ... }
// ํด๋ผ์ด์ธํธ๋ ์ด๋ฐ์์ผ๋ก ๋ณด๋ธ๋ค.
stomp.send("/app/hello", {}, "์๋
");
enableSimpleBroker() : ์๋ฒ โ ํด๋ผ์ด์ธํธ ๋ฉ์์ง ์ ๋ฌ ๊ฒฝ๋ก (๊ตฌ๋ ๊ฒฝ๋ก)
setApplicationDestinationPrefixes() : ํด๋ผ์ด์ธํธ โ ์๋ฒ ๋ฉ์์ง ์ ๋ฌ ๊ฒฝ๋ก (publish ๊ฒฝ๋ก)
/ws ์๋ํฌ์ธํธ๋ก WebSocket ์ฐ๊ฒฐ์ ์๋GET /ws HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <random_key>
Sec-WebSocket-Version: 13
Origin: http://localhost:3000
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <hashed_key>
CONNECT
accept-version:1.2
host:localhost
login:guest
passcode:guest
heart-beat:10000,10000
CONNECTED
version:1.2
heart-beat:0,0
/topic/** ๋๋ /queue/**๋ฅผ ๊ตฌ๋
(SUBSCRIBE frame)SUBSCRIBE
id:sub-0
destination:/topic/greeting
ack:auto
/app/** ๊ฒฝ๋ก๋ก ๋ฉ์์ง ๋ฐํ(SEND frame)setApplicationDestinationPrefixes("/app")๋ก ๋ผ์ฐํ
SEND
destination:/app/greeting
content-type:text/plain
content-length:25
MESSAGE
subscription:sub-0
destination:/topic/greeting
message-id:007
content-type:text/plain
content-length:25
๐ก ํฌ์ธํธ
- WebSocket ์ฐ๊ฒฐ์ด ๋จผ์ ์ฑ๋ฆฝ๋์ด์ผ STOMP CONNECT, SUBSCRIBE, SEND, MESSAGE๊ฐ ๊ฐ๋ฅ
- SUBSCRIBE์ SEND ๊ฒฝ๋ก์ ๋ฐ๋ผ Controller ์ฒ๋ฆฌ ๋๋ ๋ธ๋ก์ปค ์ง์ ์ ๋ฌ ๋ฐฉ์์ด ๊ฒฐ์ ๋จ
- ์ ์ฒด ํ๋ฆ:
WebSocket ์ฐ๊ฒฐ โ STOMP CONNECT โ SUBSCRIBE โ SEND โ ๋ธ๋ก์ปค โ MESSAGE โ ํด๋ผ์ด์ธํธ
Simple Broker๋ ํด๋ผ์ด์ธํธ ๊ตฌ๋
(subscription)์ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํ๊ณ , ํด๋น ๊ตฌ๋
์ ๊ธฐ๋ฐ์ผ๋ก ๋ฉ์์ง๋ฅผ ์ฐ๊ฒฐ๋ ํด๋ผ์ด์ธํธ์๊ฒ ์ ๋ฌํ๋ค.

| ์ฑ๋ | ์ญํ | ์ค๋ช |
|---|---|---|
| clientInboundChannel | ํด๋ผ์ด์ธํธ โ ์๋ฒ | ์น์์ผ ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ ๋ฉ์์ง๋ฅผ ์๋ฒ๋ก ์ ๋ฌํ๋ ์ฑ๋ |
| clientOutboundChannel | ์๋ฒ โ ํด๋ผ์ด์ธํธ | ์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋ผ ๋ ์ฌ์ฉํ๋ ์ฑ๋ |
| brokerChannel | ์ ํ๋ฆฌ์ผ์ด์ โ ๋ธ๋ก์ปค | ์๋ฒ(์ ํ๋ฆฌ์ผ์ด์ )์์ Simple Broker๋ก ๋ฉ์์ง๋ฅผ ์ ๋ฌํ๋ ์ฑ๋ |
Broker Relay ๋ ์ธ๋ถ STOMP ๋ธ๋ก์ปค(RabbitMQ ๋ฑ)์ TCP๋ก ์ฐ๊ฒฐ, ๋ธ๋ก์ปค๊ฐ ๊ตฌ๋
๋ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฉ์์ง ์ ๋ฌํ๋ค.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// ํด๋ผ์ด์ธํธ โ ์๋ฒ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ prefix
registry.setApplicationDestinationPrefixes("/app");
// ์ธ๋ถ STOMP ๋ธ๋ก์ปค์ TCP๋ก ์ฐ๊ฒฐ
registry.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("localhost") // ์ธ๋ถ ๋ธ๋ก์ปค ํธ์คํธ
.setRelayPort(61613) // STOMP ํฌํธ
.setClientLogin("guest") // ๋ธ๋ก์ปค ์ธ์ฆ
.setClientPasscode("guest") // ๋ธ๋ก์ปค ์ธ์ฆ
.setSystemLogin("guest") // ์์คํ
๊ณ์ (๊ด๋ฆฌ์ฉ)
.setSystemPasscode("guest")
.setVirtualHost("/"); // RabbitMQ ๋ฑ ๊ฐ์ ํธ์คํธ
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOriginPatterns("*")
.withSockJS();
}
}
| ์ฑ๋ | ์ญํ | ์ค๋ช |
|---|---|---|
| clientInboundChannel | ํด๋ผ์ด์ธํธ โ ์๋ฒ | ํด๋ผ์ด์ธํธ๊ฐ ๋ณด๋ธ STOMP ๋ฉ์์ง๋ฅผ ์์ ํ๊ณ ๋์ฝ๋ฉํ ํ, Spring Message๋ก ๋ณํ |
| clientOutboundChannel | ์๋ฒ โ ํด๋ผ์ด์ธํธ | ๋ธ๋ก์ปค๊ฐ ๋งค์นญ๋ ๊ตฌ๋ ์์๊ฒ STOMP frame์ผ๋ก ๋ฉ์์ง๋ฅผ ์ ์ก |
| brokerChannel | ์ ํ๋ฆฌ์ผ์ด์ โ ๋ธ๋ก์ปค | @Controller ๋๋ ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ธ๋ก์ปค๋ก ๋ฉ์์ง๋ฅผ ์ ๋ฌ |
ํด๋ผ์ด์ธํธ์์ ๋ฐ์ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด @Controller๋ฅผ ์ฌ์ฉํ ์ ์๋ค.
๊ฐ ์ปจํธ๋กค๋ฌ๋ ๋ค์๊ณผ ๊ฐ์ ๋ฉ์๋ ์ด๋ ธํ ์ด์ ์ ๊ฐ์ง ์ ์๋ค.
@MessageMapping โ ๋ชฉ์ ์ง ๊ธฐ๋ฐ ๋ฉ์์ง ์ฒ๋ฆฌ
@SubscribeMapping โ ๊ตฌ๋
๋ฉ์์ง ์ฒ๋ฆฌ (๋ธ๋ก์ปค๋ฅผ ๊ฑฐ์น์ง ์๊ณ ์ง์ ํด๋ผ์ด์ธํธ ์ ์ก)
`@MessageExceptionHandler1 โ ๋ฉ์์ง ์ฒ๋ฆฌ ์ค ๋ฐ์ํ ์์ธ ์ฒ๋ฆฌ
ํด๋ผ์ด์ธํธ๊ฐ /app/** ๊ฒฝ๋ก๋ก ๋ณด๋ธ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌ
์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ ํด๋์ค ๋ ๋ฒจ์ ์ ์ฉ ๊ฐ๋ฅ
@DestinationVariable์ ์ฌ์ฉํ์ฌ ๋์ ๊ฒฝ๋ก ์ฒ๋ฆฌ ๊ฐ๋ฅ
๋ฐํ๊ฐ์ MessageConverter๋ฅผ ํตํด ์ง๋ ฌํ๋์ด ๋ธ๋ก์ปค ์ฑ๋๋ก ์ ๋ฌ
๋ชฉ์ ์ง ์ปค์คํฐ๋ง์ด์ง:
@SendTo โ ์ฌ๋ฌ ๋ชฉ์ ์ง ๋๋ ๊ธฐ๋ณธ ๋ธ๋ก๋์บ์คํธ
@SendToUser โ ๋ฉ์์ง ๋ณด๋ธ ํน์ ์ฌ์ฉ์์๊ฒ๋ง ์ ์ก
@Controller
public class ChatController {
// ํด๋ผ์ด์ธํธ๊ฐ /app/chat์ผ๋ก ๋ฉ์์ง๋ฅผ ๋ณด๋์ ๋ ์ฒ๋ฆฌ
@MessageMapping("/chat")
@SendTo("/topic/messages") // ๋ชจ๋ ๊ตฌ๋
์์๊ฒ ๋ธ๋ก๋์บ์คํธ
public ChatMessage sendMessage(ChatMessage message, Principal principal) {
// ์๋ฒ์์ ๋ฐ์ ์ ์ ๋ณด ์ถ๊ฐ
message.setSender(principal.getName());
message.setTimestamp(System.currentTimeMillis());
return message;
}
// ํน์ ์ ์ ์๊ฒ๋ง ์๋ฆผ์ ๋ณด๋ด๋ ์์
@MessageMapping("/private-alert")
@SendToUser("/queue/alerts")
public Alert sendPrivateAlert(Alert alert, Principal principal) {
// ๋ก๊ทธ์ธํ ์ฌ์ฉ์์๊ฒ๋ง ์๋ฆผ
alert.setRecipient(principal.getName());
return alert;
}
}
๊ตฌ๋ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ ๋ ์ฌ์ฉ
๊ธฐ๋ณธ์ ์ผ๋ก ํด๋ผ์ด์ธํธ์๊ฒ ์ง์ ์๋ต ์ ์ก (๋ธ๋ก์ปค ์ฌ์ฉ X)
UI ์ด๊ธฐ ๋ฐ์ดํฐ ์ ๋ฌ ๋ฑ์ ์ ์ฉ
broker์ controller๋ฅผ ๋์ผ ์ ๋์ฌ์ ๋งคํํ์ง ์์ผ๋ฉด ๋ฉ์์ง ์ถฉ๋ ๋ฐฉ์ง ๊ฐ๋ฅ
@SubscribeMapping("/init")
public InitData getInitData() {
return new InitData();
}
// ๋ฉ์์ง ์ฒ๋ฆฌ ์ค ์์ธ ๋ฐ์ ์ ์ฒ๋ฆฌ
@MessageExceptionHandler
@SendToUser(destinations="/queue/errors", broadcast=false)
public ErrorMessage handleException(ChatException ex) {
return new ErrorMessage(ex.getMessage());
}
@MessageMapping ์ฒ๋ฆฌ ์ค ๋ฐ์ํ ์์ธ ์ฒ๋ฆฌ
๋ฐํ๊ฐ์ ํน์ ์ฌ์ฉ์์๊ฒ ์ ์ก ๊ฐ๋ฅ (@SendToUser)
ํด๋์ค ๋ ๋ฒจ์์๋ง ์ ์ฉํ๊ฑฐ๋ @ControllerAdvice๋ฅผ ์ฌ์ฉํด ์ ์ญ ์ฒ๋ฆฌ ๊ฐ๋ฅ
Spring WebSocket + STOMP ์๋ฒ๋ ํด๋ผ์ด์ธํธ๋ก๋ถํฐ ๋ค์ด์ค๋ ๋ฉ์์ง๋ฅผ ๋น๋๊ธฐ์ ์ผ๋ก ์ฒ๋ฆฌํ๋ค.
์ฆ, ํด๋ผ์ด์ธํธ๊ฐ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ฉด ์๋ฒ๋ ์ฆ์ ์ฒ๋ฆฌํ์ง ์๊ณ , ์ฐ๋ ๋ํ(Thread Pool) ๋ด์์ ์์ ์ ์คํํ๋ค.
์ด ๊ตฌ์กฐ ๋๋ถ์ ๋์์ ๋ง์ ํด๋ผ์ด์ธํธ๊ฐ ๋ฉ์์ง๋ฅผ ๋ณด๋ด๋ ์๋ฒ๋ ๋ธ๋กํน ์์ด ์์ ์ ์ผ๋ก ์ฒ๋ฆฌํ ์ ์๋ค.
STOMP ๋ฉ์์ง๋ ๋ด๋ถ์ ์ผ๋ก ์ฑ๋(Channel)์ ํตํด ์ฒ๋ฆฌ๋๋ฉฐ, ๊ฐ ์ฑ๋๋ณ๋ก ์ฐ๋ ๋ํ์ด ์กด์ฌํ๋ค.
| ์ฑ๋ | ์ญํ |
|---|---|
| clientInboundChannel | ํด๋ผ์ด์ธํธ โ ์๋ฒ๋ก ๋ค์ด์ค๋ ๋ฉ์์ง ์์ , ๋ผ์ฐํ , Controller ํธ์ถ ๋ฑ |
| clientOutboundChannel | ์๋ฒ โ ํด๋ผ์ด์ธํธ๋ก ๋๊ฐ๋ ๋ฉ์์ง ์ ์ก, ๋ธ๋ก์ปค ๊ตฌ๋ ์ ํ์ ํ ๋ฉ์์ง ๋ฐ์ก |
๊ฐ ์ฑ๋๋ณ ์ฐ๋ ๋ ์๋ฅผ ์ ์ ํ ์ค์ ํด์ผ ๋ฉ์์ง ๋ณ๋ชฉ์ ๋ฐฉ์งํ๊ณ ์์ ์ ์ธ ์ฒ๋ฆฌ ์๋๋ฅผ ์ ์งํ ์ ์๋ค.
โ ์ด๋ฐ ์ํฉ์์๋ ์ฑ๋๋ณ ์ฐ๋ ๋ ์๋ฅผ ๋๋ฆฌ๊ณ , ํ ์ฉ๋์ ์กฐ์ ํ์ฌ ์ฒ๋ฆฌ ์ฑ๋ฅ์ ํ๋ณดํ ์ ์๋ค.
๋ฉ์์ง ์ ์ก ์๊ฐ, ๋ฒํผ ํฌ๊ธฐ ๋ฑ์ ์ ํํ์ฌ ๋๋ฆฐ ํด๋ผ์ด์ธํธ๋ ํฐ ๋ฉ์์ง๋ก ์ธํ ์๋ฒ ์ง์ฐ์ ๋ฐฉ์งํ ์ ์๋ค.
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
registration
.setSendTimeLimit(15 * 1000) // ๋ฉ์์ง ์ ์ก ์ต๋ ์๊ฐ 15์ด
.setSendBufferSizeLimit(512 * 1024) // ์ ์ก ๋ฒํผ ์ต๋ 512KB
.setMessageSizeLimit(128 * 1024); // ๋ฉ์์ง ์ต๋ ํฌ๊ธฐ 128KB
}
setSendTimeLimit โ ๋ฉ์์ง ์ ์ก ์ต๋ ์๊ฐ ์ค์ setSendBufferSizeLimit โ ๋ฒํผ ํฌ๊ธฐ ์ ํsetMessageSizeLimit โ ํ ๋ฉ์์ง์ ์ต๋ ํฌ๊ธฐ ์ ํ
- ๋ฉ์์ง๋ ๋น๋๊ธฐ๋ก ์ฒ๋ฆฌ๋๋ฉฐ, ์ฐ๋ ๋ํ์ ํตํด ๋์์ฑ์ ํ๋ณด
- clientInboundChannel / clientOutboundChannel์ ๋ณ๋ ์ฐ๋ ๋ํ ์กด์ฌ โ ์์ /์ ์ก ์ฒ๋ฆฌ
- IO ์ง์ฐ์ด๋ ์ธ๋ถ ํธ์ถ์ด ๋ง์ ๊ฒฝ์ฐ ์ฐ๋ ๋ ์๋ฅผ ๋๋ ค ๋ณ๋ชฉ ๋ฐฉ์ง
- WebSocket ์ ์ก ์ ํ์ ํตํด ์๋ฒ ์์ ์ฑ๊ณผ ์ฒ๋ฆฌ ์ฑ๋ฅ ํ๋ณด