๐Ÿ“š [WebSocket] - WebSocket๊ณผ STOMP

CodeByHanยท2025๋…„ 12์›” 4์ผ

์Šคํ”„๋ง

๋ชฉ๋ก ๋ณด๊ธฐ
33/33

์˜ค๋Š˜์€ ๊ทธ๋ƒฅ ๊ฐ€๋ณ๊ฒŒ WebSocket์ด๋ž‘ STOMP๊ฐ€ ๋ญ”์ง€ ํ•œ ๋ฒˆ ์ •๋ฆฌํ•ด๋ณด๋ ค๊ณ  ํ•œ๋‹ค.
ํ”„๋กœ์ ํŠธ ํ•˜๋‹ค ๋ณด๋ฉด ์ฑ„ํŒ…์ด๋‚˜ ์•Œ๋ฆผ ๊ฐ™์€ ์‹ค์‹œ๊ฐ„ ๊ธฐ๋Šฅ์„ ๋„ฃ์–ด์•ผ ํ•  ๋•Œ๊ฐ€ ์žˆ๋Š”๋ฐ, ๊ทธ๋Ÿด ๋•Œ๋งˆ๋‹ค ์ •ํ™•ํžˆ ๋‘˜์˜ ์—ญํ• ์ด ๋ญ์˜€๋Š”์ง€ ์‚ด์ง ํ—ท๊ฐˆ๋ฆด ๋•Œ๊ฐ€ ์žˆ๋”๋ผ. ๊ทธ๋ž˜์„œ ์ด๋ฒˆ์— ํ•œ ๋ฒˆ ์ œ๋Œ€๋กœ ์ •๋ฆฌํ•ด๋‘๋ฉด ๋‚˜์ค‘์— ๋‹ค์‹œ ๋ณผ ๋•Œ ํŽธํ•  ๊ฒƒ ๊ฐ™์•„์„œ ์ ์–ด๋ณธ๋‹ค.

๐Ÿ“Œ WebSocket

์šฐ๋ฆฌ๊ฐ€ ํ”ํžˆ ์•Œ๊ณ  ์žˆ๋Š” 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
  • 1: ํ˜„์žฌ HTTP ํ†ต์‹ ์„ WebSocket ํ”„๋กœํ† ์ฝœ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  ์‹ถ๋‹คโ€๋ผ๋Š” ์‹ ํ˜ธ
  • 2: 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 ๋™์ž‘ํ๋ฆ„

WebSocket์€ ํฌ๊ฒŒ 3๋‹จ๊ณ„๋กœ ๋™์ž‘ํ•œ๋‹ค

  1. Handshake(HTTP ์—…๊ทธ๋ ˆ์ด๋“œ)
  2. ์–‘๋ฐฉํ–ฅ ํ”„๋ ˆ์ž„ ํ†ต์‹ 
  3. Close(์ข…๋ฃŒ ์š”์ฒญ)

1. Handshake โ€“ HTTP์—์„œ WebSocket์œผ๋กœ ํ”„๋กœํ† ์ฝœ ์ „ํ™˜

๋จผ์ € ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์„œ๋ฒ„์—๊ฒŒ ์ด๋ ‡๊ฒŒ ๋ฌป๋Š”๋‹ค:

๐Ÿ—ฃ๏ธ โ€œ์šฐ๋ฆฌ HTTP ๋ง๊ณ  WebSocket์œผ๋กœ ์–˜๊ธฐํ• ๋ž˜? Protocol Upgrade ํ• ๊ฒŒ!โ€

์ด๋•Œ ๋ธŒ๋ผ์šฐ์ €๋Š” HTTP/1.1 GET ์š”์ฒญ + Upgrade ํ—ค๋”๋ฅผ ๋ณด๋‚ธ๋‹ค.

์„œ๋ฒ„๋Š” ์š”์ฒญ์„ ๋ณด๊ณ  OK๋ผ๋ฉด

๐Ÿ—ฃ๏ธ โ€œ์ข‹์•„, ์ง€๊ธˆ๋ถ€ํ„ฐ WebSocket ํ”„๋กœํ† ์ฝœ๋กœ ์ „ํ™˜ํ•œ๋‹ค(101 Status)โ€

๋ผ๋Š” ์‘๋‹ต์„ ๋ณด๋‚ธ๋‹ค.

์—ฌ๊ธฐ๊นŒ์ง€๋Š” ์™„์ „ํžˆ HTTP ํ†ต์‹ ์ด๋‹ค.

์‘๋‹ต์ด ๋๋‚˜๋Š” ์ˆœ๊ฐ„๋ถ€ํ„ฐ๋Š” HTTP๊ฐ€ ๋Š๊ธฐ๊ณ  WebSocket ํ”„๋กœํ† ์ฝœ์ด ๋œ๋‹ค.

2. ์–‘๋ฐฉํ–ฅ ํ†ต์‹  โ€“ WebSocket Frame ์ „์†ก

Handshake๊ฐ€ ๋๋‚˜๋ฉด ์ด์ œ ์ง„์งœ WebSocket์ด ์‹œ์ž‘๋œ๋‹ค.

์ด์ œ๋ถ€ํ„ฐ๋Š” HTTP ๋ฉ”์‹œ์ง€๊ฐ€ ์•„๋‹ˆ๋ผ ํ”„๋ ˆ์ž„(Frame) ์„ ์ฃผ๊ณ ๋ฐ›๋Š”๋‹ค.

  • ๋ธŒ๋ผ์šฐ์ € โ†’ ์„œ๋ฒ„
  • ์„œ๋ฒ„ โ†’ ๋ธŒ๋ผ์šฐ์ €

์–‘์ชฝ ๋ชจ๋‘ ์š”์ฒญ/์‘๋‹ต ๊ด€๊ณ„ ์—†์ด ์ž์œ ๋กญ๊ฒŒ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

์ฆ‰, WebSocket์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ธ ์‹ค์‹œ๊ฐ„ ์–‘๋ฐฉํ–ฅ ํ†ต์‹  ๋‹จ๊ณ„

์ฃผ๊ณ ๋ฐ›๋Š” ๋ฐ์ดํ„ฐ๋Š” ๋ชจ๋‘ WebSocket ํ”„๋ ˆ์ž„ ํ˜•์‹์ด๋ฉฐ, ํ…์ŠคํŠธ ํ”„๋ ˆ์ž„ ๋˜๋Š” ๋ฐ”์ด๋„ˆ๋ฆฌ ํ”„๋ ˆ์ž„์œผ๋กœ ์ „์†ก๋œ๋‹ค.


3. Close โ€“ ์–ด๋А ํ•œ์ชฝ์—์„œ ์ข…๋ฃŒ ์š”์ฒญ

ํ†ต์‹  ์ค‘ ์–ด๋А ์ชฝ์ด๋“  โ€œ๊ทธ๋งŒํ•˜์žโ€๋ผ๊ณ  Close ํ”„๋ ˆ์ž„์„ ๋ณด๋‚ผ ์ˆ˜ ์žˆ๋‹ค.

์˜ˆ: ๋ธŒ๋ผ์šฐ์ €๊ฐ€ โ€œ๋๋‚ผ๊ฒŒ์š”(Close Frame)โ€
์„œ๋ฒ„๊ฐ€ โ€œ์˜ค์ผ€์ด(Close Response Frame)โ€ ํ•˜๊ณ  ์‘๋‹ต

๋˜๋Š” ์„œ๋ฒ„๊ฐ€ ๋จผ์ € ์ข…๋ฃŒ ์š”์ฒญํ•  ์ˆ˜๋„ ์žˆ๋‹ค.

Close ํ”„๋ ˆ์ž„์ด ์˜ค๊ฐ€๋ฉด ์—ฐ๊ฒฐ์€ ์ •์ƒ์ ์œผ๋กœ ๋‹ซํžˆ๊ณ , WebSocket ์—ฐ๊ฒฐ์ด ์ข…๋ฃŒ๋œ๋‹ค.

SocketJS

๋ฌผ๋ก  ์œ„์—์ฒ˜๋Ÿผ 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();
    }
}

์ •๋ฆฌํ•˜์ž๋ฉด,

  • WebSocket: ๋˜๋ฉด ๋น ๋ฅด๊ณ  ๊น”๋”ํ•จ
  • SockJS: ์•ˆ ๋˜๋ฉด ๋Œ€์‹  ๋‚˜์„œ์„œ WebSocket์ฒ˜๋Ÿผ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์คŒ

์ด ์ •๋„ ๋А๋‚Œ์œผ๋กœ ์ดํ•ดํ•˜๋ฉด ๋œ๋‹ค.

STOMP(Simple/Stream Text Oriented Message Protocol)

STOMP๋Š” WebSocket ์œ„์—์„œ ๋™์ž‘ํ•˜๋Š” ๋ฌธ์ž ๊ธฐ๋ฐ˜ ๋ฉ”์‹œ์ง• ํ”„๋กœํ† ์ฝœ๋กœ, ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๊ฐ€ ์ฃผ๊ณ ๋ฐ›๋Š” ๋ฉ”์‹œ์ง€์˜ ์œ ํ˜•, ํ˜•์‹, ๊ตฌ์กฐ๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ •์˜ํ•ด์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

WebSocket์€ ๋‹จ์ˆœํžˆ ์–‘๋ฐฉํ–ฅ ํ†ต์‹  ํ†ต๋กœ๋ฅผ ์—ด์–ด์ฃผ๋Š” ์ˆ˜์ค€์ด์ง€๋งŒ, STOMP๋Š” ๊ทธ ํ†ต๋กœ ์•ˆ์—์„œ ๋ฉ”์‹œ์ง€๊ฐ€ ์–ด๋–ป๊ฒŒ ์˜ค๊ฐ€์•ผ ํ•˜๋Š”์ง€์— ๋Œ€ํ•œ โ€œ๊ทœ์น™โ€์„ ์ œ๊ณตํ•œ๋‹ค๊ณ  ๋ณด๋ฉด ๋œ๋‹ค.

์ด ํ”„๋กœํ† ์ฝœ์€ TCP, WebSocket์ฒ˜๋Ÿผ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์–‘๋ฐฉํ–ฅ ์ŠคํŠธ๋ฆฌ๋ฐ ๋„คํŠธ์›Œํฌ ํ”„๋กœํ† ์ฝœ ์œ„์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ๋‹ค. STOMP ์ž์ฒด๊ฐ€ ๋ฉ”์‹œ์ง€ ์ „์†ก ๊ทœ์•ฝ๋งŒ ์ •์˜ํ•  ๋ฟ, ์‹ค์ œ ์ „์†ก์€ ํ•˜์œ„ ๋„คํŠธ์›Œํฌ ํ”„๋กœํ† ์ฝœ(WebSocket ๋“ฑ)์ด ๋‹ด๋‹นํ•œ๋‹ค.

๋˜ ํ•˜๋‚˜ ์ค‘์š”ํ•œ ํŠน์ง•์€ ๊ธฐ๋ณธ์ ์œผ๋กœ pub/sub ๊ตฌ์กฐ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋Š” ์ ์ด๋‹ค. ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋Š” ๊ณณ(SEND)๊ณผ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ๋…ํ•˜๋Š” ๊ณณ(SUBSCRIBE)์ด ๋ช…ํ™•ํ•˜๊ฒŒ ๋‚˜๋‰˜์–ด ์žˆ์–ด์„œ, ์–ด๋–ค ๋ชฉ์ ์ง€๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฐœํ–‰๋˜๊ณ , ๋ˆ„๊ฐ€ ๊ทธ ๋ชฉ์ ์ง€๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ๋ฉ”์‹œ์ง€๊ฐ€ ์ „๋‹ฌ๋˜๋Š”์ง€๊ฐ€ ํ™•์‹คํ•˜๊ฒŒ ์ •ํ•ด์ ธ ์žˆ๋‹ค.

STOMP ์—ญ์‹œ HTTP์ฒ˜๋Ÿผ ํ”„๋ ˆ์ž„(Frame) ๊ธฐ๋ฐ˜ ํ”„๋กœํ† ์ฝœ์ด๋ฉฐ, ๊ฐ ๋ฉ”์‹œ์ง€๋Š” ํ—ค๋”์™€ ๋ฐ”๋””๋ฅผ ๊ฐ–๋Š” ํŠน์ • ํ˜•์‹์˜ ํ”„๋ ˆ์ž„์œผ๋กœ ์ „์†ก๋œ๋‹ค. ๋•๋ถ„์— ๋ฉ”์‹œ์ง€๋Š” ๋‹จ์ˆœ ๋ฌธ์ž์—ด์ด ์•„๋‹ˆ๋ผ ๊ตฌ์กฐํ™”๋œ ํ˜•ํƒœ๋กœ ๊ตํ™˜๋  ์ˆ˜ ์žˆ๋‹ค.

์š”์•ฝํ•˜์ž๋ฉด, STOMP๋Š” WebSocket ์œ„์—์„œ ๋ฉ”์‹œ์ง€ ํ˜•์‹๊ณผ ๋ผ์šฐํŒ… ๊ทœ์น™์„ ์ •์˜ํ•ด์ฃผ๋Š” ๊ณ ์ˆ˜์ค€ ํ”„๋กœํ† ์ฝœ์ด๋ฉฐ, pub/sub ํŒจํ„ด์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง•์„ ๋ณด๋‹ค ์ฒด๊ณ„์ ์ด๊ณ  ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ฃผ๋Š” ์—ญํ• ์„ ํ•œ๋‹ค.

๐Ÿค” ๊ตณ์ด STOMP๊ฐ€ ํ•„์š”ํ• ๊นŒ?

์‚ฌ์‹ค WebSocket๋งŒ ๋†“๊ณ  ๋ณด๋ฉด ๊ธฐ๋Šฅ์ด ๊ต‰์žฅํžˆ ๋‹จ์ˆœํ•˜๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ ๋ฌธ์ž์—ด์„ ์ฃผ๊ณ ๋ฐ›๋Š” ์ •๋„์— ๋ถˆ๊ณผํ•˜๊ณ , ๋ฉ”์‹œ์ง€๊ฐ€ ๋ˆ„๊ตฌ์—๊ฒŒ ์ „๋‹ฌ๋ผ์•ผ ํ•˜๋Š”์ง€, ์–ด๋–ค ๋ชฉ์ ์ง€๋กœ ๋ณด๋‚ด์•ผ ํ•˜๋Š”์ง€, ์ฑ„ํŒ…๋ฐฉ ๊ฐ™์€ ๊ฐœ๋…์€ ์ „ํ˜€ ์—†๋‹ค. ์‹ฌ์ง€์–ด ๋ฉ”์‹œ์ง€ ํƒ€์ž…์กฐ์ฐจ ๊ตฌ๋ถ„๋˜์ง€ ์•Š๋Š”๋‹ค. ๋ง ๊ทธ๋Œ€๋กœ โ€œ์—ด๋ฆฐ ์†Œ์ผ“์—์„œ ๋ฌธ์ž์—ด์„ ๋ณด๋‚ด๊ณ  ๋ฐ›๋Š”๋‹คโ€๊ฐ€ ๋์ธ ์…ˆ์ด๋‹ค.

์ด๋ ‡๊ฒŒ ๋‹จ์ˆœํ•˜๋‹ค ๋ณด๋‹ˆ, ์‹ค์ œ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ๋•Œ ํ•„์š”ํ•œ ๊ธฐ๋Šฅ๋“ค์„ ์ „๋ถ€ ์ง์ ‘ ๊ตฌํ˜„ํ•ด์•ผ ํ•œ๋‹ค.

์˜ˆ๋ฅผ ๋“ค๋ฉด,

  • ํŠน์ • ์ฑ„ํŒ…๋ฐฉ์—๋งŒ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ
  • ํŠน์ • ์œ ์ €์—๊ฒŒ๋งŒ ๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ
  • ์—ฌ๋Ÿฌ ์œ ์ €์—๊ฒŒ ๋ธŒ๋กœ๋“œ์บ์ŠคํŠธํ•˜๊ธฐ
  • ๋ฉ”์‹œ์ง€ ํƒ€์ž…(์ž…์žฅ/ํ‡ด์žฅ/์ฑ„ํŒ… ๋“ฑ) ๊ตฌ๋ถ„ํ•˜๊ธฐ
  • pub/sub ๊ตฌ์กฐ ์„ค๊ณ„ํ•˜๊ธฐ

์ด๋Ÿฐ ๊ฒƒ๋“ค์„ ๋งค๋ฒˆ ์ฒ˜์Œ๋ถ€ํ„ฐ ๋งŒ๋“ค๋ ค๊ณ  ํ•˜๋ฉด ์‚ฌ์‹ค์ƒ ๊ฐœ๋ฐœ ์ง€์˜ฅ์ด ํŽผ์ณ์ง„๋‹ค.

๊ทธ๋ž˜์„œ ๋“ฑ์žฅํ•œ ๊ฒƒ์ด ๋ฐ”๋กœ STOMP๋‹ค.

STOMP๋Š” WebSocket ์œ„์—์„œ โ€œ๋ฉ”์‹œ์ง€๋ฅผ ์–ด๋–ป๊ฒŒ ๋ณด๋‚ด๊ณ  ๋ฐ›์„์ง€โ€์— ๋Œ€ํ•œ ๊ทœ์น™์„ ์ œ๊ณตํ•ด์ฃผ๋Š” ํ”„๋กœํ† ์ฝœ๋กœ, WebSocket์˜ ๋‹จ์ˆœํ•œ ํ†ต์‹  ๊ตฌ์กฐ์— ๊ตฌ๋…, ๋ฐœํ–‰, ๋ชฉ์ ์ง€ ๋ผ์šฐํŒ…, ๋ฉ”์‹œ์ง€ ๊ตฌ์กฐํ™” ๊ฐ™์€ ๊ณ ์ˆ˜์ค€ ๊ธฐ๋Šฅ์„ ์–น์–ด์ค€๋‹ค.

๋•๋ถ„์— ๊ฐœ๋ฐœ์ž๋Š” ๋ณต์žกํ•œ ๋ฉ”์‹œ์ง• ๋กœ์ง์„ ์ง์ ‘ ๋งŒ๋“ค ํ•„์š” ์—†์ด, ์ •ํ•ด์ง„ ๊ทœ์•ฝ์— ๋”ฐ๋ผ ํ›จ์”ฌ ๊น”๋”ํ•˜๊ฒŒ ์‹ค์‹œ๊ฐ„ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ค.

STOMP๋Š” WebSocket ์œ„์—์„œ ๋™์ž‘ํ•˜๋Š” โ€œ๋ฉ”์‹œ์ง€ ๊ทœ์•ฝ(ํ”„๋กœํ† ์ฝœ)โ€์ด๋‹ค. ์‰ฝ๊ฒŒ ๋งํ•˜๋ฉด, โ€œWebSocket์€ ํ†ต๋กœ๋งŒ ์—ด์–ด์ค„ ๋ฟ์ด๊ณ , STOMP๋Š” ๊ทธ ํ†ต๋กœ ์•ˆ์—์„œ ๋ฉ”์‹œ์ง€๋ฅผ ์–ด๋–ค ๊ทœ์น™์œผ๋กœ ์ฃผ๊ณ ๋ฐ›์„์ง€ ์ •ํ•ด์ฃผ๋Š” ์•ฝ์†.โ€

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 ๊ฒฝ๋กœ)

1) WebSocket ์—ฐ๊ฒฐ (Handshake)

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ /ws ์—”๋“œํฌ์ธํŠธ๋กœ WebSocket ์—ฐ๊ฒฐ์„ ์‹œ๋„
  • HTTP GET ์š”์ฒญ์œผ๋กœ WebSocket ํ•ธ๋“œ์…ฐ์ดํฌ ์ˆ˜ํ–‰ โ†’ ์„œ๋ฒ„๊ฐ€ Upgrade ์Šน์ธ
  • ์—ฐ๊ฒฐ์ด ์™„๋ฃŒ๋˜์–ด์•ผ ๊ทธ ์œ„์—์„œ STOMP ํ”„๋กœํ† ์ฝœ์ด ์‹œ์ž‘๋จ

HTTP ์š”์ฒญ ์˜ˆ์‹œ (ํด๋ผ์ด์–ธํŠธ โ†’ ์„œ๋ฒ„)

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 ์‘๋‹ต ์˜ˆ์‹œ (์„œ๋ฒ„ โ†’ ํด๋ผ์ด์–ธํŠธ)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: <hashed_key>

2) STOMP CONNECT

  • WebSocket ์—ฐ๊ฒฐ ์œ„์—์„œ STOMP CONNECT frame ์ „์†ก
  • ์„œ๋ฒ„๋Š” CONNECTED frame์œผ๋กœ STOMP ์„ธ์…˜ ์Šน์ธ

STOMP CONNECT ์˜ˆ์‹œ (ํด๋ผ์ด์–ธํŠธ โ†’ ์„œ๋ฒ„)

CONNECT
accept-version:1.2
host:localhost
login:guest
passcode:guest
heart-beat:10000,10000

STOMP CONNECTED ์˜ˆ์‹œ (์„œ๋ฒ„ โ†’ ํด๋ผ์ด์–ธํŠธ)

CONNECTED
version:1.2
heart-beat:0,0

3) SUBSCRIBE

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ /topic/** ๋˜๋Š” /queue/**๋ฅผ ๊ตฌ๋…(SUBSCRIBE frame)
  • ์„œ๋ฒ„๋Š” ๊ตฌ๋… ์ •๋ณด๋ฅผ Simple Broker ๋˜๋Š” Broker Relay์— ์ €์žฅ
  • ์ดํ›„ ํ•ด๋‹น destination์œผ๋กœ ์˜ค๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ๋… ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „๋‹ฌ

SUBSCRIBE ์˜ˆ์‹œ

SUBSCRIBE
id:sub-0
destination:/topic/greeting
ack:auto

4) SEND

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ /app/** ๊ฒฝ๋กœ๋กœ ๋ฉ”์‹œ์ง€ ๋ฐœํ–‰(SEND frame)
  • ์„œ๋ฒ„๋Š” setApplicationDestinationPrefixes("/app")๋กœ ๋ผ์šฐํŒ…
  • ํ•ด๋‹น ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•  @MessageMapping ๋ฉ”์„œ๋“œ๋กœ ์ „๋‹ฌ

SEND ์˜ˆ์‹œ

SEND
destination:/app/greeting
content-type:text/plain
content-length:25

5) ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค ์ „๋‹ฌ

  • Controller ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ(payload)๋Š” brokerChannel์„ ํ†ตํ•ด ๋ธŒ๋กœ์ปค๋กœ ์ „๋‹ฌ
  • ๋ธŒ๋กœ์ปค๋Š” ๊ตฌ๋… ์ •๋ณด๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ ์ ์ ˆํ•œ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฉ”์‹œ์ง€ ์ „์†ก

6) MESSAGE

  • ๊ตฌ๋…ํ•œ ํด๋ผ์ด์–ธํŠธ๋Š” SUBSCRIBEํ•œ destination์œผ๋กœ MESSAGE frame ์ˆ˜์‹ 

MESSAGE ์˜ˆ์‹œ

MESSAGE
subscription:sub-0
destination:/topic/greeting
message-id:007
content-type:text/plain
content-length:25
  • Simple Broker: ์„œ๋ฒ„ ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋ฐ”๋กœ ๊ตฌ๋… ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌ โ†’ clientOutboundChannel
  • Broker Relay: ์™ธ๋ถ€ ๋ธŒ๋กœ์ปค โ†’ ๊ตฌ๋… ํด๋ผ์ด์–ธํŠธ โ†’ clientOutboundChannel

๐Ÿ’ก ํฌ์ธํŠธ

  • 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 ๋˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ธŒ๋กœ์ปค๋กœ ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌ

Annotated Controllers (๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ปจํŠธ๋กค๋Ÿฌ)

ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฐ›์€ ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด @Controller๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ฐ ์ปจํŠธ๋กค๋Ÿฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋ฉ”์†Œ๋“œ ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๋‹ค.

  • @MessageMapping โ†’ ๋ชฉ์ ์ง€ ๊ธฐ๋ฐ˜ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ

  • @SubscribeMapping โ†’ ๊ตฌ๋… ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ (๋ธŒ๋กœ์ปค๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š๊ณ  ์ง์ ‘ ํด๋ผ์ด์–ธํŠธ ์ „์†ก)

  • `@MessageExceptionHandler1 โ†’ ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ๋ฐœ์ƒํ•œ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ

@MessageMapping

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ /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;
    }
}

@SubscribeMapping

  • ๊ตฌ๋… ๋ฉ”์‹œ์ง€๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์‚ฌ์šฉ

  • ๊ธฐ๋ณธ์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ง์ ‘ ์‘๋‹ต ์ „์†ก (๋ธŒ๋กœ์ปค ์‚ฌ์šฉ X)

  • UI ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋“ฑ์— ์œ ์šฉ

  • broker์™€ controller๋ฅผ ๋™์ผ ์ ‘๋‘์‚ฌ์— ๋งคํ•‘ํ•˜์ง€ ์•Š์œผ๋ฉด ๋ฉ”์‹œ์ง€ ์ถฉ๋Œ ๋ฐฉ์ง€ ๊ฐ€๋Šฅ

@SubscribeMapping("/init")
public InitData getInitData() {
    return new InitData();
}

@MessageExceptionHandler

// ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ ์˜ˆ์™ธ ๋ฐœ์ƒ ์‹œ ์ฒ˜๋ฆฌ
@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) ๋‚ด์—์„œ ์ž‘์—…์„ ์‹คํ–‰ํ•œ๋‹ค.

์ด ๊ตฌ์กฐ ๋•๋ถ„์— ๋™์‹œ์— ๋งŽ์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ฉ”์‹œ์ง€๋ฅผ ๋ณด๋‚ด๋„ ์„œ๋ฒ„๋Š” ๋ธ”๋กœํ‚น ์—†์ด ์•ˆ์ •์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

1. ์ฑ„๋„๋ณ„ ์“ฐ๋ ˆ๋“œํ’€

STOMP ๋ฉ”์‹œ์ง€๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ ์ฑ„๋„(Channel)์„ ํ†ตํ•ด ์ฒ˜๋ฆฌ๋˜๋ฉฐ, ๊ฐ ์ฑ„๋„๋ณ„๋กœ ์“ฐ๋ ˆ๋“œํ’€์ด ์กด์žฌํ•œ๋‹ค.

์ฑ„๋„์—ญํ• 
clientInboundChannelํด๋ผ์ด์–ธํŠธ โ†’ ์„œ๋ฒ„๋กœ ๋“ค์–ด์˜ค๋Š” ๋ฉ”์‹œ์ง€ ์ˆ˜์‹ , ๋ผ์šฐํŒ…, Controller ํ˜ธ์ถœ ๋“ฑ
clientOutboundChannel์„œ๋ฒ„ โ†’ ํด๋ผ์ด์–ธํŠธ๋กœ ๋‚˜๊ฐ€๋Š” ๋ฉ”์‹œ์ง€ ์ „์†ก, ๋ธŒ๋กœ์ปค ๊ตฌ๋…์ž ํƒ์ƒ‰ ํ›„ ๋ฉ”์‹œ์ง€ ๋ฐœ์†ก

๊ฐ ์ฑ„๋„๋ณ„ ์“ฐ๋ ˆ๋“œ ์ˆ˜๋ฅผ ์ ์ ˆํžˆ ์„ค์ •ํ•ด์•ผ ๋ฉ”์‹œ์ง€ ๋ณ‘๋ชฉ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์•ˆ์ •์ ์ธ ์ฒ˜๋ฆฌ ์†๋„๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.


2. ์“ฐ๋ ˆ๋“œํ’€ ์กฐ์ • ํ•„์š” ์ƒํ™ฉ

  • ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ค‘ DB ํ˜ธ์ถœ์ด๋‚˜ ์™ธ๋ถ€ API ์—ฐ๋™๊ณผ ๊ฐ™์ด IO ์ง€์—ฐ์ด ๋ฐœ์ƒํ•  ๊ฒฝ์šฐ
  • ๋‹ค์ˆ˜ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋™์‹œ์— ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•  ๊ฒฝ์šฐ
  • ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ๋Ÿ‰์„ ๋†’์—ฌ์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ

โ†’ ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ๋Š” ์ฑ„๋„๋ณ„ ์“ฐ๋ ˆ๋“œ ์ˆ˜๋ฅผ ๋Š˜๋ฆฌ๊ณ , ํ ์šฉ๋Ÿ‰์„ ์กฐ์ •ํ•˜์—ฌ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ๋‹ค.

3. WebSocket ์ „์†ก ์ œํ•œ ์„ค์ •

๋ฉ”์‹œ์ง€ ์ „์†ก ์‹œ๊ฐ„, ๋ฒ„ํผ ํฌ๊ธฐ ๋“ฑ์„ ์ œํ•œํ•˜์—ฌ ๋А๋ฆฐ ํด๋ผ์ด์–ธํŠธ๋‚˜ ํฐ ๋ฉ”์‹œ์ง€๋กœ ์ธํ•œ ์„œ๋ฒ„ ์ง€์—ฐ์„ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registration) {
    registration
        .setSendTimeLimit(15 * 1000)        // ๋ฉ”์‹œ์ง€ ์ „์†ก ์ตœ๋Œ€ ์‹œ๊ฐ„ 15์ดˆ
        .setSendBufferSizeLimit(512 * 1024) // ์ „์†ก ๋ฒ„ํผ ์ตœ๋Œ€ 512KB
        .setMessageSizeLimit(128 * 1024);   // ๋ฉ”์‹œ์ง€ ์ตœ๋Œ€ ํฌ๊ธฐ 128KB
}
  • setSendTimeLimit โ†’ ๋ฉ”์‹œ์ง€ ์ „์†ก ์ตœ๋Œ€ ์‹œ๊ฐ„ ์„ค์ •
  • setSendBufferSizeLimit โ†’ ๋ฒ„ํผ ํฌ๊ธฐ ์ œํ•œ
  • setMessageSizeLimit โ†’ ํ•œ ๋ฉ”์‹œ์ง€์˜ ์ตœ๋Œ€ ํฌ๊ธฐ ์ œํ•œ
  1. ๋ฉ”์‹œ์ง€๋Š” ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ๋˜๋ฉฐ, ์“ฐ๋ ˆ๋“œํ’€์„ ํ†ตํ•ด ๋™์‹œ์„ฑ์„ ํ™•๋ณด
  2. clientInboundChannel / clientOutboundChannel์— ๋ณ„๋„ ์“ฐ๋ ˆ๋“œํ’€ ์กด์žฌ โ†’ ์ˆ˜์‹ /์ „์†ก ์ฒ˜๋ฆฌ
  3. IO ์ง€์—ฐ์ด๋‚˜ ์™ธ๋ถ€ ํ˜ธ์ถœ์ด ๋งŽ์€ ๊ฒฝ์šฐ ์“ฐ๋ ˆ๋“œ ์ˆ˜๋ฅผ ๋Š˜๋ ค ๋ณ‘๋ชฉ ๋ฐฉ์ง€
  4. WebSocket ์ „์†ก ์ œํ•œ์„ ํ†ตํ•ด ์„œ๋ฒ„ ์•ˆ์ •์„ฑ๊ณผ ์ฒ˜๋ฆฌ ์„ฑ๋Šฅ ํ™•๋ณด

์ฐธ๊ณ 

profile
๋…ธ๋ ฅ์€ ๋ฐฐ์‹ ํ•˜์ง€ ์•Š์•„ ๐Ÿ”ฅ

0๊ฐœ์˜ ๋Œ“๊ธ€