์๋ฒ ๊ด๋ จ ์์ ์ฝ๋๋ 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 ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๊ฒ ์ต๋๋ค.
์ฐธ๊ณ
- HTML Standard - Last-Event-ID
- MDN ํ๊ธํ: Server-Sent Events ์ฌ์ฉํ๊ธฐ
- ์ฐ์ํ ๊ธฐ์ ๋ธ๋ก๊ทธ: Server-Sent Events๋ก ์ค์๊ฐ ์๋ฆผ ์ ๋ฌํ๊ธฐ
- Sionic AI: SSE๋ก ์ค์๊ฐ ๋ฐ์ดํฐ ์ ์กํ๊ธฐ
- ํ ์ฝ๋ธ: Spring์์ Server-Sent-Events ๊ตฌํํ๊ธฐ
- Baeldung: Spring MVC Streaming and SSE Request Processing