SSE๋ก ์๋ฆผ ๊ธฐ๋ฅ ๊ตฌํํ๊ธฐ with Spring
์๋ก
์ธํฐ๋ท์ ์น ๋ธ๋ผ์ฐ์ ์ ์น ์๋ฒ ๊ฐ์ ๋ฐ์ดํฐ ํต์ ์ ์ํด์ HTTP ํ์ค ์์ ๊ตฌ์ถ๋์ด ์์ต๋๋ค. ๋๋ถ๋ถ์ ๊ฒฝ์ฐ ์น ๋ธ๋ผ์ฐ์ ์ธ ํด๋ผ์ด์ธํธ๊ฐ HTTP ์์ฒญ์ ์๋ฒ์ ๋ณด๋ด๊ณ , ์๋ฒ๋ ์ ์ ํ ์๋ต์ ๋ฐํํ๋๋ฐ ์ด๋ฐ ์๋ณต ํต์ ์ 'https://www.google.com'๊ณผ ๊ฐ์ ์ฃผ์๋ฅผ ๋ธ๋ผ์ฐ์ ์ ์ ๋ ฅํ์ ๋ ์น ํ์ด์ง๋ฅผ ๋ฐ๊ฒ ๋๋ ๊ณผ์ ์ ๋๋ค.
์ด๋ฌํ HTTP ํ์ค์ ๊ด๋ฒ์ํ๊ฒ ์ง์๋์ง๋ง, ์ ํ๋ฆฌ์ผ์ด์ ์ด ์ฐ์์ ์ธ ์ ๋ณด๋ฅผ ์๋ฒ์ ์ ์กํ๊ฑฐ๋, ์ค์๊ฐ์ผ๋ก ์ ๋ฐ์ดํธ๋ ์๋ฒ์ ์ ๋ณด๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ๋ณด๋ด์ผ ํ๋ ๊ฒฝ์ฐ ์ง์์ ์ธ HTTP ์์ฒญ์ ํ๊ฒ ๋๊ธฐ์ ๋น์ฉ๋ฉด์์ ๋งค์ฐ ๋น ํจ์จ์ ์ ๋๋ค. ์ด๋ฐ ์ํฉ์์ ํด๋ง, ์น์์ผ, ๊ทธ๋ฆฌ๊ณ SSE๊ฐ ๋ฑ์ฅํ๋๋ฐ, ์ด๋ค์ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ์๋์ ๋ฉ๋ชจ๋ฆฌ ํจ์จ์ฑ์ ์ค์ ์ ๋ ํ๋กํ ์ฝ ๋ค์ ๋๋ค.
- Short Polling
- Long Polling
- SSE(Server-Sent-Events)
- WebSocket
์ด๋ฒ ๊ฒ์๊ธ์์๋ ์ ํ๋กํ ์ฝ๋ค์ ๊ฐ๋
์ ๋ํด์ ๊ฐ๋ตํ๊ฒ ์ค๋ช
ํ๊ณ , Spring์์ SSE๋ฅผ ํ์ฉํด ์๋ฆผ ๊ธฐ๋ฅ์ ๊ตฌํํ๋ ์์๋ฅผ ์์ฑํด ๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
1. Short Polling
Short Polling์ ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ๋ก๋ถํฐ ์ ๊ธฐ์ ์ธ ์ ๋ณด๋ฅผ ๋ฐ๊ธฐ ์ํ ํ๋กํ ์ฝ์ ๋๋ค. ํด๋ผ์ด์ธํธ๊ฐ ์ฃผ๊ธฐ์ ์ผ๋ก ์๋ฒ๋ก ์์ฒญ์ ๋ณด๋ด ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๋ฐฉ๋ฒ์ ๋๋ค.
๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์๋ก์ด ์ ๋ณด์ ๋ํ HTTP ์์ฒญ์ ๋ณด๋ธ๋ค.
- ์๋ฒ๋ ์๋ก์ด ์ ๋ณด๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ๋ค.
- ํด๋ผ์ด์ธํธ๋ ์ค์ ํ ์ฃผ๊ธฐ(์:2์ด)๋ก ์์ฒญ์ ๋ฐ๋ณตํ๋ค.
ํด๋ผ์ด์ธํธ๊ฐ Http Request๋ฅผ ์๋ฒ๋ก ๊ณ์ ๋ณด๋ด์ ์ด๋ฒคํธ ๋ด์ฉ์ ์ ๋ฌ๋ฐ๋ ๋ฐฉ์์ด๋ฉฐ, ํด๋ผ์ด์ธํธ๊ฐ ์ง์์ ์ผ๋ก Request๋ฅผ ์๋ฒ์ ๋ณด๋ด๊ธฐ ๋๋ฌธ์ ํด๋ผ์ด์ธํธ๊ฐ ๋ง์์ง๋ฉด ์๋ฒ์ ๋ถ๋ด์ด ์ฆ๊ฐํ๊ฒ ๋ฉ๋๋ค. ๋ํ TCP์ connection์ ๋งบ๊ณ ๋๋ ๊ฒ ์์ฒด๊ฐ ๋งค์ฐ ๋ฌด๊ฒ๊ณ , ์ด์ ๋ฐ๋ผ ์ค์๊ฐ์ผ๋ก ๋ณํ๋๋ ๋น ๋ฅธ ์ ๋ณด์ ์๋ต์ ๊ธฐ๋ํ๊ธฐ๋ ์ด๋ ต์ต๋๋ค.
ํ์ง๋ง ํด๋ผ์ด์ธํธ์ ์๋ฒ์ ๊ตฌํ์ด ๋ชจ๋ ๋จ์ํ๋ค๋ ์ฅ์ ์ด ์๊ณ , ์๋ฒ๊ฐ ์์ฒญ์ ๋ํ ๋ถ๋ด์ด ํฌ์ง ์๊ณ ์์ฒญ ์ฃผ๊ธฐ๋ฅผ ๋๋ํ๊ฒ ์ก์๋ ๋ ์ ๋๋ก ์ค์๊ฐ์ฑ์ด ์ค์ํ์ง ์๋ค๋ฉด ๊ณ ๋ คํด ๋ณผ ๋งํ ๋ฐฉ๋ฒ์
๋๋ค.
2. Long Polling
Long Polling์ Short Polling ๋ณด๋ค๋ ๋ ํจ์จ์ ์ธ ๋ฐฉ์์ ๋๋ค.
Short Polling ๋ณด๋ค ์๋ฒ์์ ๊ธฐ๋ค๋ฆฌ๋ ์๊ฐ์ด ๋ ๊ธธ์ด์ง ๊ฒ์ด ๋ณด์ด์๋์? ๊ณผ์ ์ Short Polling์์ ๋ง์ง๋ง๋ง ๋ค๋ฆ ๋๋ค.
- ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ์๋ก์ด ์ ๋ณด์ ๋ํ HTTP ์์ฒญ์ ๋ณด๋ธ๋ค.
- ์๋ฒ๋ ์๋ก์ด ์ ๋ณด๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ๋ฐํํ๋ค.
- ํด๋ผ์ด์ธํธ๋ ์ด์ ์๋ต์ ๋ฐ์๋ง์ ๋ค์ ์์ฒญ์ ๋ฐ๋ณตํ๋ค.
Long Polling์ Short Polling์ ๋นํด ๋์ผํ ์์ ๋ฐ์ดํฐ๋ฅผ ํด๋ผ์ด์ธํธ์๊ฒ ์ ์กํ๋๋ฐ ํ์ํ HTTP ์์ฒญ ์๋ฅผ ์ค์ผ ์ ์์ต๋๋ค. ํ์ง๋ง ์๋ฒ๋ก๋ถํฐ ์๋ต์ ๋ฐ๊ณ ๋๋ฉด ๋ค์ ์ฐ๊ฒฐ ์์ฒญ์ ํ๊ธฐ ๋๋ฌธ์, ์ํ๊ฐ ๋น๋ฒํ๊ฒ ๋ฐ๋๋ค๋ฉด ์ฐ๊ฒฐ ์์ฒญ๋ ๋์ด๋ ์๋ฒ์ ๋ถ๋ด์ด ๊ฐ๋ ๊ฒ์ ๋ณํ์ง ์์ต๋๋ค.
๋ฐ๋ผ์ ์ค์๊ฐ ๋ฉ์์ง ์ ๋ฌ์ด ์ค์ํ์ง๋ง ์๋ฒ์ ์ํ๊ฐ ๋น๋ฒํ๊ฒ ๋ณํ์ง ์๋ ๊ฒฝ์ฐ์ ์ ํฉํฉ๋๋ค.
3. SSE(Server-Sent-Events)
SSE๋ ์๋ฒ์ ํ๋ฒ ์ฐ๊ฒฐ์ ๋งบ๊ณ ๋๋ฉด, ์ผ์ ์๊ฐ ๋์ ์๋ฒ์์ ๋ณ๊ฒฝ์ด ๋ฐ์ํ ๋๋ง๋ค ์๋ฒ์์ ํด๋ผ์ด์ธํธ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ํด๋ผ์ด์ธํธ๋ ์๋ฒ๋ฅผ ๊ตฌ๋ ํ๋ค.(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์ ๊ณผ์ ์ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
- ํด๋ผ์ด์ธํธ์ ์๋ฒ๋ 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์ฐ๊ฒฐ ๋์ ํธ๋์ญ์ ์ ๊ณ์ ๋ฌผ๊ณ ์์ด ์ปค๋ฅ์ ๋ญ๋น๊ฐ ์ผ์ด๋ ์ ์์ผ๋ ํธ๋์ญ์ ์ ๊ฑธ์ง ์์์ผ ํฉ๋๋ค.