๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

CS/Web, Network

SSE(Server-Sent Events) Spec ์€ ์ •ํ•ด์ ธ ์žˆ๋‹ค

์„œ๋ฒ„ ๊ด€๋ จ ์˜ˆ์‹œ ์ฝ”๋“œ๋Š” Java/Spring ์œผ๋กœ ์ง„ํ–‰๋˜๋ฉฐ, SSE Spec(์‘๋‹ต ํ˜•์‹)์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๋Š” ๊ฒƒ์œผ๋กœ ์„œ๋ฒ„์—์„œ SSE ๊ตฌํ˜„ ๋ฐฉ๋ฒ•์—๋Š” ๋‹ค์Œ ๊ธ€์„ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

 

ํด๋ผ์ด์–ธํŠธ ์˜ˆ์‹œ ์ฝ”๋“œ์— ๋Œ€ํ•ด ๋” ์•Œ๊ณ ์‹ถ๋‹ค๋ฉด '์ฐธ๊ณ ' ์˜ ์ƒ๋‹จ 2๊ฐœ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š”.

 

0. SSE ์˜ Spec์€ ์ •ํ•ด์ ธ ์žˆ๋‹ค.

SSE๋Š” HTML ์ŠคํŽ™(WHATWG) ์— ์ •์˜๋˜์–ด ์žˆ๊ณ , HTTP ์ŠคํŽ™(IETF)๊ณผ๋Š” ๋ณ„๊ฐœ์ž…๋‹ˆ๋‹ค.

WHATWG → SSE (EventSource, Event Stream ํฌ๋งท) ์ •์˜
IETF   → HTTP/1.1, HTTP/2, HTTP/3 ์ •์˜

 

HTTP ๋ฒ„์ „์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง€๋Š” ๊ฑด SSE ์˜ Spec์ด ์•„๋‹ˆ๋ผ ๊ทธ๊ฑธ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด HTTP/2.0 ๊ณผ HTTP/1.1 ์€ ๋ฐ์ดํ„ฐ ์ „๋‹ฌ ๋ฐฉ์‹์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

SSE ๋ฅผ HTTP/2.0 ์œผ๋กœ ์„œ๋น™ํ•  ๋•Œ๋Š” HTTP/2.0 ์ด ํ•˜๋‚˜์˜ TCP ์—ฐ๊ฒฐ๋กœ ์ง€์†ํ•˜๊ธฐ ๋•Œ๋ฌธ์— keep-alive ํ—ค๋”, Transfer-Encoding ํ—ค๋” ๋“ฑ์„ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ, SSE ๋ฅผ HTTP/1.1 ๋กœ ์„œ๋น™ํ•  ๋•Œ๋Š” keep-alive ํ—ค๋”, Transfer-Encoding ํ—ค๋”๊ฐ€ ์ถ”๊ฐ€๋ฉ๋‹ˆ๋‹ค.

 

์ฆ‰ id:, data:, event:, retry: ํ•„๋“œ๋กœ ๊ตฌ์„ฑ๋œ SSE ์˜ Spec ์€ HTTP/1.1์ด๋“  HTTP/2๋“  HTTP/3์ด๋“  ๋™์ผํ•˜๊ฒŒ ์ ์šฉ๋ฉ๋‹ˆ๋‹ค. ํฌ๋งท์€ ๊ทธ๋Œ€๋กœ๊ณ , ์–ด๋–ป๊ฒŒ ์ŠคํŠธ๋ฆผ ๋ฐ์ดํ„ฐ๋ฅผ ์ „๋‹ฌํ• ์ง€๋ฅผ HTTP ๋ฒ„์ „์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

 

* ์•„๋ž˜ ๊ธ€์€ HTTP/1.1 ์„ ๊ธฐ์ค€์œผ๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

1. Response Header

์„œ๋ฒ„๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋‚ด๋ ค์ฃผ๋Š” Response ํ—ค๋”๋Š” ์•„๋ž˜์™€ ๊ฐ™์Šต๋‹ˆ๋‹ค.

header("X-Accel-Buffering: no");
header("Content-Type: text/event-stream");
header("Cache-Control: no-cache");

 

Content-Type ๊ณผ Cache-Control ์€ ์›Œ๋‚™ ์œ ๋ช…ํ•˜์ง€๋งŒ, X-Accel-Buffering ์€ ์ƒ์†Œํ•ฉ๋‹ˆ๋‹ค.

 

X-Accel-Buffering ํ—ค๋”๋Š” Nginx๋งŒ์ด ์ธ์‹ํ•  ์ˆ˜ ์žˆ๋Š” ํ—ค๋”๋กœ, Nginx๋กœ ํ•˜์—ฌ๊ธˆ ์‘๋‹ต์„ ๋ฒ„ํผ์— ๋ชจ์•˜๋‹ค๊ฐ€ ํ•œ ๋ฒˆ์— ๋ณด๋‚ด์ง€ ์•Š๋„๋ก ํ•˜๋Š” ์„ค์ •์— ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ์ ์œผ๋กœ Nginx๋Š” upstream(๋ฐ์ดํ„ฐ ๊ทผ์›์ง€, ์˜ˆ๋ฅผ ๋“ค๋ฉด Spring Boot ์„œ๋ฒ„)์˜ ์‘๋‹ต์„ ๋ฒ„ํผ์— ๋ชจ์•˜๋‹ค๊ฐ€ ํ•œ ๋ฒˆ์— ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌํ•˜๋Š”๋ฐ, ์ด๋Š” ๋„คํŠธ์›Œํฌ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ธฐ ์œ„ํ•จ์ด๋ฉฐ ์ผ๋ฐ˜์ ์ธ HTTP ์‘๋‹ต(JSON, HTML ๋“ฑ)์—์„œ๋Š” ์ด ๋ฐฉ์‹์ด ์œ ๋ฆฌํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

ํ•˜์ง€๋งŒ SSE ๊ฐ™์ด ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ํ˜๋ ค์•ผ ํ•˜๋ฏ€๋กœ ๋ฒ„ํผ๋ง์„ ์‚ฌ์šฉ๋˜๋ฉด ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ์ด ๋•Œ ์„œ๋ฒ„์—์„œ X-Accel-Buffering: no ํ—ค๋”๋ฅผ ์‘๋‹ต์— ํฌํ•จํ•˜๋ฉด, Nginx๊ฐ€ ํ•ด๋‹น ์‘๋‹ต์—๋งŒ ๋ฒ„ํผ๋ง์„ ๋„๊ณ  ์‹ค์‹œ๊ฐ„์œผ๋กœ ํ˜๋ฆฝ๋‹ˆ๋‹ค.

 

๊ทธ๋ฆฌ๊ณ , Nginx ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ http 1.0 ์„ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— http1.1 ์˜ keep-alive ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก http version ๋„ ์„ค์ •ํ•ด์ค˜์•ผ ํ•˜๋ฉฐ, Connection ๋„ close ๋˜์ง€ ์•Š๋„๋ก ์ถ”๊ฐ€ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

๊ทธ๋ž˜์„œ Nginx ์ „์šฉ(๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ ์ „์šฉ ํ—ค๋”)์ธ X-Accel-Buffering ํ—ค๋”๋„ ์„œ๋ฒ„๊ฐ€ ๋‚ด๋ ค์ฃผ๋Š” ๊ฒƒ์ด ์•„๋‹Œ Nginx ์— ํ•จ๊ป˜ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์ด ์œ ์ง€๋ณด์ˆ˜ ์ธก๋ฉด์—์„œ ์ข‹๋‹ค๊ณ  ์ƒ๊ฐํ•˜๋ฉฐ X-Accel-Buffering ํ—ค๋”๋ฅผ Nginx ์˜ config ํŒŒ์ผ์— ์„ค์ •ํ•ด์ค์‹œ๋‹ค.  

X-Accel-Buffering ์€ Nginx ์ „์šฉ ํ—ค๋”์ž…๋‹ˆ๋‹ค.
Nginx ๋Œ€์‹  HAProxy๋‚˜ Apache HTTP Server๋ฅผ ๋ฆฌ๋ฒ„์Šค ํ”„๋ก์‹œ๋กœ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ๋ณ„๋„์˜ ์„ค์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.
์˜ˆ๋ฅผ ๋“ค์–ด HAProxy server sent event timeout ์œผ๋กœ ๊ฒ€์ƒ‰ํ•˜๋ฉด ์‰ฝ๊ฒŒ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

2. Response Body

SSE์˜ Response Body ํ•„๋“œ๋Š” id, event, data, retry๋กœ ๊ตฌ์„ฑ๋˜๋ฉฐ ๊ทธ ์™ธ ๋ชจ๋“  ํ•„๋“œ๋Š” ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.

 

id ํ•„๋“œ

`id`๋Š” ์ปค๋„ฅ์…˜์ด ๋Š๊ฒผ๋‹ค๊ฐ€ ์žฌ์—ฐ๊ฒฐ ์‹œ ํด๋ผ์ด์–ธํŠธ๊ฐ€ `Last-Event-ID` ํ—ค๋”๋กœ ๋งˆ์ง€๋ง‰์œผ๋กœ ๋ฐ›์€ id๋ฅผ ์„œ๋ฒ„์— ๋ณด๋‚ด์คŒ์œผ๋กœ์จ, ์–ด๋””์„œ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๋ณด๋‚ผ์ง€ ํ™•์ธํ•˜๋Š” ์šฉ๋„์ž…๋‹ˆ๋‹ค.

 

๋ธŒ๋ผ์šฐ์ €(ํด๋ผ์ด์–ธํŠธ)๋Š” ๋„คํŠธ์›Œํฌ๊ฐ€ ๋Š๊ธฐ๊ฑฐ๋‚˜ ์„œ๋ฒ„๊ฐ€ ๋‹ค์šด๋˜๋ฉด, SSE ์‘๋‹ต์„ ๋ฐ›๊ธฐ ์œ„ํ•ด `retry` ํ•„๋“œ์˜ ๊ฐ’(๋‹จ์œ„ ms)๋งˆ๋‹ค ๋ฐ˜๋ณตํ•ด์„œ ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜๊ฒŒ ๋˜๋Š”๋ฐ, ์•„๋ž˜์ฒ˜๋Ÿผ Last-Event-ID๋ฅผ ํฌํ•จํ•ด์„œ ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.(default ๋™์ž‘ ๋ฐฉ์‹)

 

Spring(์„œ๋ฒ„) ์€ ์•„๋ž˜์™€ ๊ฐ™์ด ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

@GetMapping(value = "/mvc/last-event-id", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> lastEventIdDemo(
        @RequestHeader(value = "Last-Event-ID", required = false) Integer lastEventId
) {

 

 

๋ฌธ์ œ๋Š” ์„œ๋ฒ„๊ฐ€ ๊ณ„์† Event Stream ์‘๋‹ต์„ ๋ณด๋‚ด๊ณ  ์žˆ๋Š”๋ฐ, ๋„คํŠธ์›Œํฌ๊ฐ€ ์ผ์‹œ์ ์œผ๋กœ ๋Š๊ฒผ์„ ๋•Œ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ์‹ค๋  ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค.

 

์˜ˆ๋ฅผ ๋“ค์–ด ์„œ๋ฒ„๋Š” id 1๋ถ€ํ„ฐ 10๊นŒ์ง€์˜ ๋ฐ์ดํ„ฐ ์ค‘ 6๋ถ€ํ„ฐ ๋ณด๋ƒˆ๋Š”๋ฐ, ํด๋ผ์ด์–ธํŠธ๋Š” ๋„คํŠธ์›Œํฌ ์žฅ์• ๋กœ ์ธํ•ด ์ปค๋„ฅ์…˜์ด ์ž ๊น ๋Š์ผœ 4๊นŒ์ง€ ๋ฐ›์•˜์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด, ํด๋ผ์ด์–ธํŠธ(๋ธŒ๋ผ์šฐ์ €)๋Š” ์ž๋™์œผ๋กœ ์ปค๋„ฅ์…˜ ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜๊ฒŒ ๋˜๋ฉฐ(๊ธฐ๋ณธ ๋™์ž‘์ž…๋‹ˆ๋‹ค), Last-Event-ID์— 4๋ฅผ ๋„ฃ์–ด ์„œ๋ฒ„์—๊ฒŒ ์‘๋‹ต์„ ๋‹ค์‹œ ์š”์ฒญํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

์ด ์ƒํƒœ์—์„œ ์„œ๋ฒ„๊ฐ€ 4๋ถ€ํ„ฐ ๋‹ค์‹œ ์ „๋‹ฌํ•˜๋ ค๋ฉด Event Stream ์‘๋‹ต ๋ฐ์ดํ„ฐ๊ฐ€ ์–ด๋”˜๊ฐ€์— ์ €์žฅ๋˜์–ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฏธ ์„œ๋ฒ„๋Š” 6๊นŒ์ง€ ๋ณด๋ƒˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด ์„œ๋ฒ„๊ฐ€ ๋‚ด๋ ค์ค„ ์‘๋‹ตbody๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•ด ๋ฉ”๋ชจ๋ฆฌ, Redis, Kafka, DB ๋“ฑ ๋‹ค์–‘ํ•œ ์„ ํƒ์ง€๋ฅผ ๊ณ ๋ คํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

์ด์ฒ˜๋Ÿผ Last-Event-ID(id ํ•„๋“œ)๋ฅผ ์ด์šฉํ•ด ํŠน์ • ์‹œ์ ๋ถ€ํ„ฐ ๋‹ค์‹œ Event Stream ์‘๋‹ต์„ ๋ฐ›์œผ๋ ค๋ฉด Event Stream ๋ฐ์ดํ„ฐ๋ฅผ ์žฌ์ƒ ๊ฐ€๋Šฅํ•˜๊ฒŒ๋กœ ๊ตฌํ˜„ํ•ด์•ผ ํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

 

event ํ•„๋“œ

์ด๋ฒคํŠธ ์œ ํ˜•์„ ์‹๋ณ„ํ•˜๋Š” ๋ฌธ์ž์—ด์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ’์ด ์ง€์ •๋˜์–ด ์žˆ๋‹ค๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” ์ง€์ •๋œ ์ด๋ฒคํŠธ ์ด๋ฆ„์— ๋Œ€ํ•œ ๋ฆฌ์Šค๋„ˆ์—๊ฒŒ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์›น ์‚ฌ์ดํŠธ ์†Œ์Šค ์ฝ”๋“œ๊ฐ€ ์ด๋ฆ„์„ ๊ฐ–๋Š” ์ด๋ฒคํŠธ๋ฅผ ์ˆ˜์‹ ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” addEventListener()๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ฉ”์‹œ์ง€์— ์ด๋ฒคํŠธ ์ด๋ฆ„์ด ์ง€์ •๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ onmessage ํ•ธ๋“ค๋Ÿฌ๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

 

์„œ๋ฒ„ ์˜ˆ์‹œ๋Š” Spring MVC๊ฐ€ Event Stream Spec์„ ์ž˜ ๋ณด์—ฌ์ฃผ๋Š” SseEmitter ํด๋ž˜์Šค๋ฅผ ์ด์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์„œ๋ฒ„๊ฐ€ ์•„๋ž˜์™€ ๊ฐ™์ด Event Stream ์‘๋‹ต์„ ๋งŒ๋“ค์–ด ์ฃผ๊ณ  ์žˆ๋‹ค๋ฉด,

SseEmitter emitter = new SseEmitter(60_000L);
emitter.send(SseEmitter.event()
    .id(String.valueOf(i))
    .name("sse-event")
    .data("์ด๋ฒคํŠธ #" + i + " [" + connectionLabel + "]")
    .reconnectTime(2000)); // ๋Š๊ธฐ๋ฉด 2์ดˆ ํ›„ ์žฌ์—ฐ๊ฒฐ

 

๋ธŒ๋ผ์šฐ์ €๋Š” ์„œ๋ฒ„๊ฐ€ ๋ณด๋‚ด์ค€ "sse-event"๋ผ๋Š” ๋ฌธ์ž์—ด์„ event ํ•„๋“œ๋กœ ์ˆ˜์‹ ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

 

 

JS์—์„œ๋Š” "sse-event" ์ด๋ฒคํŠธ๋ฅผ ๋ฐ›์•˜์„ ๋•Œ ์‹คํ–‰ํ•  ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

EventSource evtSource = new EventSource("์„œ๋ฒ„ URL");
evtSource.addEventListener("sse-event", function (event) {
  ...
});

 

๋งŒ์•ฝ, ์„œ๋ฒ„๊ฐ€ event ํ•„๋“œ๋ฅผ ์ฃผ์ง€ ์•Š๋Š”๋‹ค๋ฉด

SseEmitter emitter = new SseEmitter(60_000L);
emitter.send(SseEmitter.event()
    .id(String.valueOf(i))
    .data("์ด๋ฒคํŠธ #" + i + " [" + connectionLabel + "]")
    .reconnectTime(2000)); // ๋Š๊ธฐ๋ฉด 2์ดˆ ํ›„ ์žฌ์—ฐ๊ฒฐ

 

๋ธŒ๋ผ์šฐ์ €๋Š” event ํ•„๋“œ๋ฅผ ๋ฐ›์ง€ ๋ชปํ•˜๊ณ , message ์ด๋ฒคํŠธ๋กœ ์ˆ˜์‹ ์ด ๋˜๋ฉฐ,

 

JS์—์„œ๋Š” onmessage ๋กœ message ์ด๋ฒคํŠธ๋ฅผ ์œ„ํ•œ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

evtSource.onmessage = function (e) {
  ...
};

 

data ํ•„๋“œ

Event Stream ์‘๋‹ต์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋“ค์–ด๊ฐ‘๋‹ˆ๋‹ค. SseEmitter๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ .data() ๋ฉ”์„œ๋“œ์— ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์–ด์ค๋‹ˆ๋‹ค.

 

retry ํ•„๋“œ

์žฌ์—ฐ๊ฒฐ ๋Œ€๊ธฐ ์‹œ๊ฐ„์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„์™€์˜ ์—ฐ๊ฒฐ์ด ๋Š์–ด์ง€๋ฉด ๋ธŒ๋ผ์šฐ์ €๋Š” ์žฌ์—ฐ๊ฒฐ์„ ์‹œ๋„ํ•˜๊ธฐ ์ „์— ์ง€์ •๋œ ์‹œ๊ฐ„์„ ๊ธฐ๋‹ค๋ฆฝ๋‹ˆ๋‹ค. ์ด ๊ฐ’์€ ๋ฐ€๋ฆฌ์ดˆ ๋‹จ์œ„์˜ ์ •์ˆ˜์—ฌ์•ผ ํ•˜๋ฉฐ, ์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฐ’์ด ์ง€์ •๋œ ๊ฒฝ์šฐ ์ด ํ•„๋“œ๋Š” ๋ฌด์‹œ๋ฉ๋‹ˆ๋‹ค.

 

3. ๋ฒˆ์™ธ

SseEmitter์˜ comment

SseEmitter์—๋Š” Event Stream Spec์—๋Š” ์—†๋Š” .comment() ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

@GetMapping(value = "/mvc/sse-emitter")
public SseEmitter sseEmitter() throws IOException {
    SseEmitter emitter = new SseEmitter();
    emitter.send(SseEmitter.event()
        .id("id-1")
        .data("Hello World")
        .name("event ํ•„๋“œ")
        .comment("event ์ฝ”๋ฉ˜ํŠธ")
        .reconnectTime(10000));
    return emitter;
}

 

Window Powershell ์—์„œ curl ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉด ์•„๋ž˜์™€ ๊ฐ™์€ ์‘๋‹ต์„ ๋ฐ›์Šต๋‹ˆ๋‹ค.

PS C:...\tech-playground> curl.exe -N http://localhost:8080/mvc/sse-emitter
id:id-1
data:Hello World
event:event ํ•„๋“œ
:event ์ฝ”๋ฉ˜ํŠธ
retry:10000

 

์ฆ‰, .comment()๋Š” :event ์ฝ”๋ฉ˜ํŠธ ํ˜•ํƒœ๋กœ ์น˜ํ™˜๋˜๋ฉฐ, ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž๋„๊ตฌ์˜ EventStream ํƒญ์—์„œ๋Š” ํ™•์ธํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.

 

 

๊ทธ ์ด์œ ๋Š” :์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ์ค„์€ Event Stream Spec์ƒ ์ฃผ์„์œผ๋กœ ์ทจ๊ธ‰๋˜์–ด ๋ฌด์‹œ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

 

.comment()๋Š” ์ฃผ๋กœ heartbeat(์—ฐ๊ฒฐ ์œ ์ง€ ์‹ ํ˜ธ) ์šฉ๋„๋กœ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. Nginx ๊ฐ™์€ ํ”„๋ก์‹œ๋Š” ์ผ์ • ์‹œ๊ฐ„ ๋™์•ˆ ์•„๋ฌด ๋ฐ์ดํ„ฐ๋„ ์˜ค์ง€ ์•Š์œผ๋ฉด idle ์ปค๋„ฅ์…˜์œผ๋กœ ํŒ๋‹จํ•ด ๋Š์–ด๋ฒ„๋ฆฌ๋Š”๋ฐ, heartbeat comment๋ฅผ ์ฃผ๊ธฐ์ ์œผ๋กœ ๋ณด๋‚ด๋ฉด ๋ฐ์ดํ„ฐ๊ฐ€ ํ๋ฅด๊ณ  ์žˆ๋‹ค๊ณ  ์ธ์‹ํ•ด ๋Š์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๋ธŒ๋ผ์šฐ์ €๋Š” Event Stream Spec์— ๋”ฐ๋ผ ์ด ๋ฐ์ดํ„ฐ๋ฅผ ์ฃผ์„์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋ฏ€๋กœ ์‹ค์ œ ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ๋กœ์ง์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

 

SSE ์ข…๋ฃŒ ์•Œ๋ฆผ

ํด๋ผ์ด์–ธํŠธ๋Š” SSE ์‘๋‹ต์ด ์–ธ์ œ ๋๋‚ ์ง€ ๋ชจ๋ฆ…๋‹ˆ๋‹ค. ์ด๋ฅผ ์œ„ํ•ด event ํ•„๋“œ๋ฅผ ํ†ตํ•ด ํŠน์ • ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด SSE ์‘๋‹ต์ด ์ข…๋ฃŒ๋˜์—ˆ๋‹ค๊ณ  ์„œ๋ฒ„๊ฐ€ ์•Œ๋ ค์ฃผ๋Š” ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

 

Spring MVC์˜ Streaming ๋ฐฉ์‹ ๋น„๊ต

Spring MVC ์˜ Streaming ๋ฐฉ์‹์€ SseEmitter ์™€ StreamingResponseBody ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์—‘์…€ ๋‹ค์šด๋กœ๋“œ ๊ธฐ๋Šฅ๊ฐ™์ด ํŒŒ์ผ์„ ๋‹ค์šด๋กœ๋“œ ํ•  ๋•Œ๋Š” ResponseEntity<StreamingResponseBody> ๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ์—์„œ return ํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

  SseEmitter StreamingResponseBody
ํฌ๋งท SSE ์ŠคํŽ™ ์ž๋™ ์ ์šฉ raw ๋ฐ”์ดํŠธ ์ง์ ‘ ์ž‘์„ฑ
์šฉ๋„ SSE ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆฌ๋ฐ ํŒŒ์ผ ๋‹ค์šด๋กœ๋“œ, ๋Œ€์šฉ๋Ÿ‰ ๋ฐ์ดํ„ฐ
ํด๋ผ์ด์–ธํŠธ EventSource fetch, ์ผ๋ฐ˜ HTTP
ํฌ๋งท ์ฒ˜๋ฆฌ ์ž๋™ ์ง์ ‘

 

๋™์‹œ SSE ์—ฐ๊ฒฐ ์ œํ•œ

HTTP/2์—์„œ ๋™์‹œ SSE ์—ฐ๊ฒฐ ๊ฐœ์ˆ˜๋Š” ๊ธฐ๋ณธ๊ฐ’ 100๊ฐœ์ž…๋‹ˆ๋‹ค. ๊ทธ ์•„๋ž˜ HTTP ๋ฒ„์ „์€ ๋„๋ฉ”์ธ๋‹น 6๊ฐœ๋กœ ๋งค์šฐ ๋‚ฎ๊ฒŒ ์„ค์ •๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋งŒ์•ฝ, ํ•œ ์‚ฌ์šฉ์ž๋‹น ๋™์‹œ SSE ์ปค๋„ฅ์…˜ ์—ฐ๊ฒฐ์„ ์ œํ•œํ•ด์•ผ ํ•œ๋‹ค๋ฉด, ์„œ๋ฒ„ ๋˜๋Š” JS์—์„œ ์‚ฌ์šฉ์ž ๋‹น 1๊ฐœ์˜ SSE ์ปค๋„ฅ์…˜์„ ๊ฐ€์ง€๋„๋ก ์ถ”๊ฐ€ ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ฐธ๊ณ  ์˜์ƒ ๋ฐ ์‹ค์Šต ์ฝ”๋“œ

๋Œ€๊ทœ๋ชจ ๋Œ€๊ธฐ์—ด์—์„œ์˜ WebSocket vs polling ๋„ ๊ฐ™์ด ์˜์ƒ์„ ๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

์—„์ฒญ๋‚œ ํด๋ง ํšŸ์ˆ˜. ๊ณผ์—ฐ ์›น์†Œ์ผ“์— ๋น„ํ•ด ์œ ๋ฆฌํ•œ๊ฐ€?

 

SSE ๊ด€๋ จ ์‹ค์Šต ์ฝ”๋“œ๋Š” GitHub์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

 

๋‹ค์Œ ๊ธ€๋กœ๋Š” Spring ์—์„œ SSE ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

 

์ฐธ๊ณ