๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ
BackEnd๐ŸŒฑ/Spring

SSE๋กœ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ with Spring

by dkswnkk 2023. 6. 18.

์„œ๋ก 

์ธํ„ฐ๋„ท์€ ์›น ๋ธŒ๋ผ์šฐ์ €์™€ ์›น ์„œ๋ฒ„ ๊ฐ„์˜ ๋ฐ์ดํ„ฐ ํ†ต์‹ ์„ ์œ„ํ•ด์„œ HTTP ํ‘œ์ค€ ์œ„์— ๊ตฌ์ถ•๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์›น ๋ธŒ๋ผ์šฐ์ €์ธ ํด๋ผ์ด์–ธํŠธ๊ฐ€ HTTP ์š”์ฒญ์„ ์„œ๋ฒ„์— ๋ณด๋‚ด๊ณ , ์„œ๋ฒ„๋Š” ์ ์ ˆํ•œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•˜๋Š”๋ฐ ์ด๋Ÿฐ ์™•๋ณต ํ†ต์‹ ์€ 'https://www.google.com'๊ณผ ๊ฐ™์€ ์ฃผ์†Œ๋ฅผ ๋ธŒ๋ผ์šฐ์ €์— ์ž…๋ ฅํ–ˆ์„ ๋•Œ ์›น ํŽ˜์ด์ง€๋ฅผ ๋ฐ›๊ฒŒ ๋˜๋Š” ๊ณผ์ •์ž…๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ HTTP ํ‘œ์ค€์€ ๊ด‘๋ฒ”์œ„ํ•˜๊ฒŒ ์ง€์›๋˜์ง€๋งŒ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์—ฐ์†์ ์ธ ์ •๋ณด๋ฅผ ์„œ๋ฒ„์— ์ „์†กํ•˜๊ฑฐ๋‚˜, ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋œ ์„œ๋ฒ„์˜ ์ •๋ณด๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ณด๋‚ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ ์ง€์†์ ์ธ HTTP ์š”์ฒญ์„ ํ•˜๊ฒŒ ๋˜๊ธฐ์— ๋น„์šฉ๋ฉด์—์„œ ๋งค์šฐ ๋น„ ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. ์ด๋Ÿฐ ์ƒํ™ฉ์—์„œ ํด๋ง, ์›น์†Œ์ผ“, ๊ทธ๋ฆฌ๊ณ  SSE๊ฐ€ ๋“ฑ์žฅํ–ˆ๋Š”๋ฐ, ์ด๋“ค์€ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์˜ ์†๋„์™€ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ์— ์ค‘์ ์„ ๋‘” ํ”„๋กœํ† ์ฝœ ๋“ค์ž…๋‹ˆ๋‹ค.

  1. Short Polling
  2. Long Polling
  3. SSE(Server-Sent-Events)
  4. WebSocket

์ด๋ฒˆ ๊ฒŒ์‹œ๊ธ€์—์„œ๋Š” ์œ„ ํ”„๋กœํ† ์ฝœ๋“ค์˜ ๊ฐœ๋…์— ๋Œ€ํ•ด์„œ ๊ฐ„๋žตํ•˜๊ฒŒ ์„ค๋ช…ํ•˜๊ณ , Spring์—์„œ SSE๋ฅผ ํ™œ์šฉํ•ด ์•Œ๋ฆผ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ์˜ˆ์‹œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.
 

1. Short Polling 

Short Polling์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์ •๊ธฐ์ ์ธ ์ •๋ณด๋ฅผ ๋ฐ›๊ธฐ ์œ„ํ•œ ํ”„๋กœํ† ์ฝœ์ž…๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ฃผ๊ธฐ์ ์œผ๋กœ ์„œ๋ฒ„๋กœ ์š”์ฒญ์„ ๋ณด๋‚ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ค๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

Short Polling

๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ์ƒˆ๋กœ์šด ์ •๋ณด์— ๋Œ€ํ•œ HTTP ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค.
  • ์„œ๋ฒ„๋Š” ์ƒˆ๋กœ์šด ์ •๋ณด๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ๋Š” ์„ค์ •ํ•œ ์ฃผ๊ธฐ(์˜ˆ:2์ดˆ)๋กœ ์š”์ฒญ์„ ๋ฐ˜๋ณตํ•œ๋‹ค.

ํด๋ผ์ด์–ธํŠธ๊ฐ€ Http Request๋ฅผ ์„œ๋ฒ„๋กœ ๊ณ„์† ๋ณด๋‚ด์„œ ์ด๋ฒคํŠธ ๋‚ด์šฉ์„ ์ „๋‹ฌ๋ฐ›๋Š” ๋ฐฉ์‹์ด๋ฉฐ, ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ง€์†์ ์œผ๋กœ Request๋ฅผ ์„œ๋ฒ„์— ๋ณด๋‚ด๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋งŽ์•„์ง€๋ฉด ์„œ๋ฒ„์˜ ๋ถ€๋‹ด์ด ์ฆ๊ฐ€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ๋˜ํ•œ TCP์˜ connection์„ ๋งบ๊ณ  ๋Š๋Š” ๊ฒƒ ์ž์ฒด๊ฐ€ ๋งค์šฐ ๋ฌด๊ฒ๊ณ , ์ด์— ๋”ฐ๋ผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ณ€ํ™”๋˜๋Š” ๋น ๋ฅธ ์ •๋ณด์˜ ์‘๋‹ต์„ ๊ธฐ๋Œ€ํ•˜๊ธฐ๋Š” ์–ด๋ ต์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„์˜ ๊ตฌํ˜„์ด ๋ชจ๋‘ ๋‹จ์ˆœํ•˜๋‹ค๋Š” ์žฅ์ ์ด ์žˆ๊ณ , ์„œ๋ฒ„๊ฐ€ ์š”์ฒญ์— ๋Œ€ํ•œ ๋ถ€๋‹ด์ด ํฌ์ง€ ์•Š๊ณ  ์š”์ฒญ ์ฃผ๊ธฐ๋ฅผ ๋„‰๋„‰ํ•˜๊ฒŒ ์žก์•„๋„ ๋  ์ •๋„๋กœ ์‹ค์‹œ๊ฐ„์„ฑ์ด ์ค‘์š”ํ•˜์ง€ ์•Š๋‹ค๋ฉด ๊ณ ๋ คํ•ด ๋ณผ ๋งŒํ•œ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.
 

2. Long Polling

Long Polling์€ Short Polling ๋ณด๋‹ค๋Š” ๋” ํšจ์œจ์ ์ธ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค.

Long Polling

Short Polling ๋ณด๋‹ค ์„œ๋ฒ„์—์„œ ๊ธฐ๋‹ค๋ฆฌ๋Š” ์‹œ๊ฐ„์ด ๋” ๊ธธ์–ด์ง„ ๊ฒƒ์ด ๋ณด์ด์‹œ๋‚˜์š”? ๊ณผ์ •์€ Short Polling์—์„œ ๋งˆ์ง€๋ง‰๋งŒ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

  • ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์— ์ƒˆ๋กœ์šด ์ •๋ณด์— ๋Œ€ํ•œ HTTP ์š”์ฒญ์„ ๋ณด๋‚ธ๋‹ค.
  • ์„œ๋ฒ„๋Š” ์ƒˆ๋กœ์šด ์ •๋ณด๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•œ๋‹ค.
  • ํด๋ผ์ด์–ธํŠธ๋Š” ์ด์ „ ์‘๋‹ต์„ ๋ฐ›์ž๋งˆ์ž ๋‹ค์‹œ ์š”์ฒญ์„ ๋ฐ˜๋ณตํ•œ๋‹ค.

Long Polling์€ Short Polling์— ๋น„ํ•ด ๋™์ผํ•œ ์–‘์˜ ๋ฐ์ดํ„ฐ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ์ „์†กํ•˜๋Š”๋ฐ ํ•„์š”ํ•œ HTTP ์š”์ฒญ ์ˆ˜๋ฅผ ์ค„์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ์„œ๋ฒ„๋กœ๋ถ€ํ„ฐ ์‘๋‹ต์„ ๋ฐ›๊ณ  ๋‚˜๋ฉด ๋‹ค์‹œ ์—ฐ๊ฒฐ ์š”์ฒญ์„ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์ƒํƒœ๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ฐ”๋€๋‹ค๋ฉด ์—ฐ๊ฒฐ ์š”์ฒญ๋„ ๋Š˜์–ด๋‚˜ ์„œ๋ฒ„์— ๋ถ€๋‹ด์ด ๊ฐ€๋Š” ๊ฒƒ์€ ๋ณ€ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ ์‹ค์‹œ๊ฐ„ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ์ด ์ค‘์š”ํ•˜์ง€๋งŒ ์„œ๋ฒ„์˜ ์ƒํƒœ๊ฐ€ ๋นˆ๋ฒˆํ•˜๊ฒŒ ๋ณ€ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.
 

3. SSE(Server-Sent-Events)

SSE๋Š” ์„œ๋ฒ„์™€ ํ•œ๋ฒˆ ์—ฐ๊ฒฐ์„ ๋งบ๊ณ  ๋‚˜๋ฉด, ์ผ์ • ์‹œ๊ฐ„ ๋™์•ˆ ์„œ๋ฒ„์—์„œ ๋ณ€๊ฒฝ์ด ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

SSE(Server-Sent-Events)

๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ํด๋ผ์ด์–ธํŠธ๋Š” ์„œ๋ฒ„๋ฅผ ๊ตฌ๋…ํ•œ๋‹ค.(SSE Connection์„ ๋งบ๋Š”๋‹ค.)
  • ์„œ๋ฒ„๋Š” ๋ณ€๋™์‚ฌํ•ญ์ด ์ƒ๊ธธ ๋•Œ๋งˆ๋‹ค ๊ตฌ๋…ํ•œ ํด๋ผ์ด์–ธํŠธ๋“ค์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•œ๋‹ค.

SSE๋Š” ์ƒํ™ฉ์— ๋”ฐ๋ผ์„œ ์‘๋‹ต๋งˆ๋‹ค ๋‹ค์‹œ ์š”์ฒญ์„ ํ•ด์•ผ ํ•˜๋Š” Long Polling ๋ฐฉ์‹๋ณด๋‹ค ํšจ์œจ์ ์ž…๋‹ˆ๋‹ค. SSE๋Š” ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ text message๋ฅผ ๋ณด๋‚ด๋Š” ๋ธŒ๋ผ์šฐ์ € ๊ธฐ๋ฐ˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ธฐ์ˆ ์ด๋ฉฐ HTTP์˜ persistent connections์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜๋Š” HTML5 ํ‘œ์ค€ ๊ธฐ์ˆ ์ž…๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ HTTP๋ฅผ ํ†ตํ•œ SSE(HTTP/2๊ฐ€ ์•„๋‹ ๊ฒฝ์šฐ)๋Š” ๋ธŒ๋ผ์šฐ์ € ๋‹น 6๊ฐœ์˜ ์—ฐ๊ฒฐ๋กœ ์ œํ•œ๋˜๋ฏ€๋กœ, ์‚ฌ์šฉ์ž๊ฐ€ ์›น ์‚ฌ์ดํŠธ์˜ ์—ฌ๋Ÿฌ ํƒญ์„ ์—ด๋ฉด ์ฒซ 6๊ฐœ์˜ ํƒญ ์ดํ›„์—๋Š” SSE๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ๋‹จ์ ๋„ ์žˆ๊ธด ํ•ฉ๋‹ˆ๋‹ค. (HTTP/2์—์„œ๋Š” 100๊ฐœ๊นŒ์ง€์˜ ์ ‘์†์„ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค.)

์ „๋ฐ˜์ ์œผ๋กœ SSE๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„œ๋ฒ„์™€ ํฌ๊ฒŒ ํ†ต์‹ ํ•  ํ•„์š” ์—†์ด ๋‹จ์ง€ ์—…๋ฐ์ดํŠธ๋œ ๋ฐ์ดํ„ฐ๋งŒ ๋ฐ›์•„์•ผ ํ•˜๋Š” ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ŠคํŠธ๋ฆผ์— ๋Œ€ํ•œ ๊ตฌํ˜„์ด ํ•„์š”ํ•  ๋•Œ๋Š” ๋งค์šฐ ํ›Œ๋ฅญํ•œ ์„ ํƒ์ž…๋‹ˆ๋‹ค.
 

4. WebSockets

WebSockets์€ OSI ๋„คํŠธ์›Œํฌ ๋ชจ๋ธ์˜ 4 ๊ณ„์ธต ํ”„๋กœํ† ์ฝœ์ธ TCP์— ๊ธฐ๋ฐ˜ํ•œ ์–‘๋ฐฉํ–ฅ ๋ฉ”์‹œ์ง€ ์ „๋‹ฌ ํ”„๋กœํ† ์ฝœ์ž…๋‹ˆ๋‹ค. WebSockets์€ ํ”„๋กœํ† ์ฝœ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ์ ๊ณ , ๋„คํŠธ์›Œํฌ ์Šคํƒ์—์„œ ๋” ๋‚ฎ์€ ์ˆ˜์ค€์—์„œ ๋™์ž‘ํ•˜๊ธฐ ๋•Œ๋ฌธ์— HTTP๋ณด๋‹ค ๋ฐ์ดํ„ฐ ์ „์†ก์ด ๋น ๋ฆ…๋‹ˆ๋‹ค.

WebSockets

์ƒ์œ„ ์ˆ˜์ค€์—์„œ ๋ณด๋ฉด WebSockets์˜ ๊ณผ์ •์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

  • ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๋Š” HTTP๋ฅผ ํ†ตํ•ด ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜๊ณ , WebSockets ํ•ธ๋“œ์…ฐ์ดํฌ๋ฅผ ํ†ตํ•ด ์—ฐ๊ฒฐํ•œ๋‹ค.
  • WebSockets์˜ ๋ฐ์ดํ„ฐ๋Š” ํด๋ผ์ด์–ธํŠธ ์„œ๋ฒ„์™€ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ์ „์†ก๋œ๋‹ค.

์›น ์†Œ์ผ“์˜ ๊ฐ€์žฅ ํฐ ์žฅ์ ์€ ์†๋„์ž…๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„๋Š” ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•  ๋•Œ๋งˆ๋‹ค ์„œ๋กœ์˜ ์—ฐ๊ฒฐ์„ ์ฐพ์•„์„œ ๋‹ค์‹œ ์„ค์ •ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ์›น ์†Œ์ผ“ ์—ฐ๊ฒฐ์ด ์„ค์ •๋˜๋ฉด ๋ฐ์ดํ„ฐ๋Š” ์–ด๋Š ๋ฐฉํ–ฅ์œผ๋กœ๋“  ์ฆ‰์‹œ ์•ˆ์ „ํ•˜๊ฒŒ ์ „์†ก๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.(TCP์ด๊ธฐ์— ๋ฉ”์‹œ์ง€๊ฐ€ ํ•ญ์ƒ ์ˆœ์„œ๋Œ€๋กœ ๋„์ฐฉํ•˜๋„๋ก ๋ณด์žฅ๋จ)

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

๋”ฐ๋ผ์„œ ์ „๋ฐ˜์ ์œผ๋กœ ์›น ์†Œ์ผ“์€ ๋น ๋ฅธ ๊ณ ํ’ˆ์งˆ์˜ ์–‘๋ฐฉํ–ฅ ์—ฐ๊ฒฐ์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์ข‹์€ ์„ ํƒ์ด์ง€๋งŒ, ์‹œ์Šคํ…œ์— ์ƒ๋‹นํ•œ ๋ณต์žก์„ฑ์„ ์ถ”๊ฐ€ํ•˜๊ณ  ๊ตฌํ˜„ํ•˜๋Š” ๋ฐ ๋งŽ์€ ํˆฌ์ž๊ฐ€ ํ•„์š”ํ•˜๋ฏ€๋กœ ํด๋ง์ด๋‚˜ SSE๊ฐ€ ์ ํ•ฉํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.
 

Spring์—์„œ SSE๋กœ ์•Œ๋ฆผ ๊ธฐ๋Šฅ ๊ตฌํ˜„ํ•˜๊ธฐ

์„œ๋ก ์ด ๊ธธ์—ˆ์Šต๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ด์ œ ๋ฐ”๋กœ Spring์—์„œ SSE๋ฅผ ํ™œ์šฉํ•ด ์•Œ๋ฆผ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์ž‘์„ฑํ•ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.
์ „์ฒด ์ฝ”๋“œ๋Š” ๊นƒํ—ˆ๋ธŒ์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

NotificationController

@RestController
@RequestMapping("/notifications")
@RequiredArgsConstructor
public class NotificationController {
    private final NotificationService notificationService;

    @GetMapping(value = "/subscribe/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter subscribe(@PathVariable Long id) {
        return notificationService.subscribe(id);
    }

    @PostMapping("/send-data/{id}")
    public void sendData(@PathVariable Long id) {
        notificationService.notify(id, "data");
    }
}

๋จผ์ € NotificationController์ž…๋‹ˆ๋‹ค. ๊ฐ„๋‹จํ•˜๊ฒŒ ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ตฌ๋…์„ ํ•˜๊ธฐ ์œ„ํ•œ subcribe ๋ฉ”์„œ๋“œ์™€ ์ž„์‹œ๋กœ ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์•Œ๋ฆผ์„ ์ฃผ๊ธฐ ์œ„ํ•œ sendData ๋ฉ”์„œ๋“œ๋ฅผ ์ƒ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค.

NotificationService

@Service
@RequiredArgsConstructor
public class NotificationService {
    // ๊ธฐ๋ณธ ํƒ€์ž„์•„์›ƒ ์„ค์ •
    private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60;

    private final EmitterRepository emitterRepository;

    /**
     * ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ตฌ๋…์„ ์œ„ํ•ด ํ˜ธ์ถœํ•˜๋Š” ๋ฉ”์„œ๋“œ.
     *
     * @param userId - ๊ตฌ๋…ํ•˜๋Š” ํด๋ผ์ด์–ธํŠธ์˜ ์‚ฌ์šฉ์ž ์•„์ด๋””.
     * @return SseEmitter - ์„œ๋ฒ„์—์„œ ๋ณด๋‚ธ ์ด๋ฒคํŠธ Emitter
     */
    public SseEmitter subscribe(Long userId) {
        SseEmitter emitter = createEmitter(userId);

        sendToClient(userId, "EventStream Created. [userId=" + userId + "]");
        return emitter;
    }

    /**
     * ์„œ๋ฒ„์˜ ์ด๋ฒคํŠธ๋ฅผ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ณด๋‚ด๋Š” ๋ฉ”์„œ๋“œ
     * ๋‹ค๋ฅธ ์„œ๋น„์Šค ๋กœ์ง์—์„œ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด ๋ฐ์ดํ„ฐ๋ฅผ Object event์— ๋„ฃ๊ณ  ์ „์†กํ•˜๋ฉด ๋œ๋‹ค.
     *
     * @param userId - ๋ฉ”์„ธ์ง€๋ฅผ ์ „์†กํ•  ์‚ฌ์šฉ์ž์˜ ์•„์ด๋””.
     * @param event  - ์ „์†กํ•  ์ด๋ฒคํŠธ ๊ฐ์ฒด.
     */
    public void notify(Long userId, Object event) {
        sendToClient(userId, event);
    }

    /**
     * ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†ก
     *
     * @param id   - ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์„ ์‚ฌ์šฉ์ž์˜ ์•„์ด๋””.
     * @param data - ์ „์†กํ•  ๋ฐ์ดํ„ฐ.
     */
    private void sendToClient(Long id, Object data) {
        SseEmitter emitter = emitterRepository.get(id);
        if (emitter != null) {
            try {
                emitter.send(SseEmitter.event().id(String.valueOf(id)).name("sse").data(data));
            } catch (IOException exception) {
                emitterRepository.deleteById(id);
                emitter.completeWithError(exception);
            }
        }
    }

    /**
     * ์‚ฌ์šฉ์ž ์•„์ด๋””๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ด๋ฒคํŠธ Emitter๋ฅผ ์ƒ์„ฑ
     *
     * @param id - ์‚ฌ์šฉ์ž ์•„์ด๋””.
     * @return SseEmitter - ์ƒ์„ฑ๋œ ์ด๋ฒคํŠธ Emitter.
     */
    private SseEmitter createEmitter(Long id) {
        SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT);
        emitterRepository.save(id, emitter);

        // Emitter๊ฐ€ ์™„๋ฃŒ๋  ๋•Œ(๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ „์†ก๋œ ์ƒํƒœ) Emitter๋ฅผ ์‚ญ์ œํ•œ๋‹ค.
        emitter.onCompletion(() -> emitterRepository.deleteById(id));
        // Emitter๊ฐ€ ํƒ€์ž„์•„์›ƒ ๋˜์—ˆ์„ ๋•Œ(์ง€์ •๋œ ์‹œ๊ฐ„๋™์•ˆ ์–ด๋– ํ•œ ์ด๋ฒคํŠธ๋„ ์ „์†ก๋˜์ง€ ์•Š์•˜์„ ๋•Œ) Emitter๋ฅผ ์‚ญ์ œํ•œ๋‹ค.
        emitter.onTimeout(() -> emitterRepository.deleteById(id));

        return emitter;
    }
}

๋‹ค์Œ์€ NotificationService์ž…๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฒ˜์Œ ๊ตฌ๋… ์‹œ, ์ฆ‰ subscribe ๋ฉ”์„œ๋“œ์—์„œ ์ฒ˜์Œ ๊ตฌ๋… ์‹œ์— sendToClient()๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๋Š” ์ด์œ ๋Š” ์ฒ˜์Œ SSE ์‘๋‹ต์„ ํ•  ๋•Œ ์•„๋ฌด๋Ÿฐ ์ด๋ฒคํŠธ๋„ ๋ณด๋‚ด์ง€ ์•Š์œผ๋ฉด ์žฌ์—ฐ๊ฒฐ ์š”์ฒญ์„ ๋ณด๋‚ด๊ฑฐ๋‚˜, ์—ฐ๊ฒฐ ์š”์ฒญ ์ž์ฒด์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ ์ฒซ SSE ์‘๋‹ต์„ ๋ณด๋‚ผ ์‹œ ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ๋„ฃ์–ด ์ด๋Ÿฌํ•œ ์˜ค๋ฅ˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

EmitterRepository

@Repository
@RequiredArgsConstructor
public class EmitterRepository {
    // ๋ชจ๋“  Emitters๋ฅผ ์ €์žฅํ•˜๋Š” ConcurrentHashMap
    private final Map<Long, SseEmitter> emitters = new ConcurrentHashMap<>();

    /**
     * ์ฃผ์–ด์ง„ ์•„์ด๋””์™€ ์ด๋ฏธํ„ฐ๋ฅผ ์ €์žฅ
     *
     * @param id      - ์‚ฌ์šฉ์ž ์•„์ด๋””.
     * @param emitter - ์ด๋ฒคํŠธ Emitter.
     */
    public void save(Long id, SseEmitter emitter) {
        emitters.put(id, emitter);
    }

    /**
     * ์ฃผ์–ด์ง„ ์•„์ด๋””์˜ Emitter๋ฅผ ์ œ๊ฑฐ
     *
     * @param id - ์‚ฌ์šฉ์ž ์•„์ด๋””.
     */
    public void deleteById(Long id) {
        emitters.remove(id);
    }

    /**
     * ์ฃผ์–ด์ง„ ์•„์ด๋””์˜ Emitter๋ฅผ ๊ฐ€์ ธ์˜ด.
     *
     * @param id - ์‚ฌ์šฉ์ž ์•„์ด๋””.
     * @return SseEmitter - ์ด๋ฒคํŠธ Emitter.
     */
    public SseEmitter get(Long id) {
        return emitters.get(id);
    }
}

๋‹ค์Œ์€ EmitterRepository์ž…๋‹ˆ๋‹ค. ๋ชจ๋“  Emitter๋“ค์„ ์ €์žฅํ•˜๋Š” emitters๊ฐ์ฒด๋ฅผ thread-safe ํ•œ ConturrentHashMap์„ ํ†ตํ•ด ์„ ์–ธํ–ˆ์Šต๋‹ˆ๋‹ค.
 

ํ…Œ์ŠคํŠธ 

๊ตฌ๋…ํ•˜๊ธฐ

๋จผ์ € GET "/subscribe/{id}"๋ฅผ ํ†ตํ•ด ๊ตฌ๋…์„ ์‹œ๋„ํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ์ถœ๋ ฅ๋ฉ๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ๋ฐœํ–‰ํ•˜๊ธฐ

๊ทธ๋ฆฌ๊ณ  POST "/send-data/{id}"๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ ์ž„์‹œ๋กœ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰์‹œํ‚ต๋‹ˆ๋‹ค.

๋ฐœํ–‰๋œ ๋ฐ์ดํ„ฐ ์ „์†ก์™”๋Š”์ง€ ํ™•์ธํ•˜๊ธฐ

๊ทธ๋Ÿฌ๋ฉด ๊ตฌ๋…ํ•œ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ด๋ฒคํŠธ๋ฅผ ์ „์†ก๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํ…Œ์ŠคํŠธ๋ฅผ ํ•ด๋ณผ๊นŒ์š”? ํด๋ผ์ด์–ธํŠธ๋Š” ๋จผ์ € ์•„๋ž˜์™€ ๊ฐ™์€ ์ด๋ฒคํŠธ ๊ตฌ๋…์„ ์œ„ํ•ด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ

const eventSource = new EventSource('http://localhost:8080/notifications/subscribe/1');

eventSource.addEventListener('sse', event => {
    console.log(event);
});

WebConfig

@Configuration
public class WebConfig implements WebMvcConfigurer {
    public static final String ALLOWED_METHOD_NAMES = "GET,HEAD,POST,PUT,DELETE,TRACE,OPTIONS,PATCH";

    @Override
    public void addCorsMappings(final CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods(ALLOWED_METHOD_NAMES.split(","));
    }
}

cors ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜๋„ ์žˆ์œผ๋‹ˆ ์„œ๋ฒ„์—์„œ ์ž„์‹œ์ ์œผ๋กœ cors๋ฅผ disable ์ฒ˜๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ๊ตฌ๋…

๊ทธ๋ฆฌ๊ณ  console ๋ถ€๋ถ„์— js ์ฝ”๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ์‚ฝ์ž…ํ•˜๋ฉด ์ด๋ฒคํŠธ ๊ตฌ๋…์ด ์ •์ƒ์ ์œผ๋กœ ๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Response๋ฅผ ๋ณด๋ฉด Content-Type์ด text/event-stream์œผ๋กœ ๋œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ ๋ฐœํ–‰ํ•˜๊ธฐ

๊ทธ๋ฆฌ๊ณ  ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐœํ–‰์‹œ์ผœ ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ํ™•์ธ

์ด๋ฒคํŠธ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์ˆ˜์‹ ํ•œ ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. 
 

๊ฒฐ๋ก 

๋‹จ์ˆœ ์•Œ๋ฆผ ํ™•์ธ API๋ฅผ ๋งŒ๋“ค์–ด ๋†“๊ณ  ํ”„๋ก ํŠธ์—์„œ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋„๋ก ํ•˜๋Š” Polling ๋ฐฉ์‹์ด ๊ตฌํ˜„์—๋Š” ๊ฐ„๋‹จํ•˜๊ธด ํ•˜์ง€๋งŒ, ์—ฌ์œ ๊ฐ€ ๋œ๋‹ค๋ฉด SSE ๋ฐฉ์‹์œผ๋กœ ํ•ด๋ณด๋Š” ๊ฒƒ๋„ ์ข‹์€ ๊ฒฝํ—˜์ด ๋  ๊ฒƒ์ด๋ผ ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ ์œ„์˜ ์ฝ”๋“œ๋“ค์€ ์ •๋ง ๋‹จ์ˆœํ•œ ์˜ˆ์ œ์ฝ”๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์— ์‹ค ์„œ๋น„์Šค์—์„œ ์šด์˜ํ•  ๊ฒƒ์ด๋ผ๋ฉด ์กฐ๊ธˆ ๋” ์•„ํ‹ฐํด์„ ์ฐพ์•„๋ณด๊ณ  ์ ์šฉํ•  ๊ฒƒ์„ ๊ถŒ์žฅ๋“œ๋ฆฝ๋‹ˆ๋‹ค. ๋‹ค์ค‘ WAS ํ™˜๊ฒฝ์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ์ฝ”๋“œ์ด๊ธฐ ๋•Œ๋ฌธ์— Redis์˜ Pub/Sub๋‚˜ Kafka๋ฅผ ๋ง๋ถ™์—ฌ์„œ ์‚ฌ์šฉํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค. 

์ถ”๊ฐ€๋กœ, OSIV๊ฐ€ ์ผœ์ ธ์žˆ์„ ์‹œ SSE ์„œ๋น„์Šค๋‹จ์— ํŠธ๋žœ์žญ์…˜์ด ๊ฑธ๋ ค์žˆ๋‹ค๋ฉด SSE์—ฐ๊ฒฐ ๋™์•ˆ ํŠธ๋žœ์žญ์…˜์„ ๊ณ„์† ๋ฌผ๊ณ  ์žˆ์–ด ์ปค๋„ฅ์…˜ ๋‚ญ๋น„๊ฐ€ ์ผ์–ด๋‚  ์ˆ˜ ์žˆ์œผ๋‹ˆ ํŠธ๋žœ์žญ์…˜์„ ๊ฑธ์ง€ ์•Š์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค.

 

์ฐธ๊ณ 

๋Œ“๊ธ€