작성중입니다. → 완전히 다적힐때까지 조금만 기다려주세요...
<피드백 언제나 환영합니다! 댓글 감사합니다>
알림을 보내줘야할때 또는 실시간으로 데이터를 보내줘야 할 경우가 있습니다. 그때 사용하는 기술중 하나인 SSE(Server Sent Event)를 알아볼 예정입니다.
실시간 웹앱 개발시 사용되는 방법
실제 실시간 통신에서 사용되는 기술들이 있습니다. 그중 3가지를 뽑아 간단하게 알아 보겠습니다.
- Polling(client pull)
클라이언트가 일정한 주기로 서버에 업데이트 요청을 보내는 방법, 지속적인 HTTP 요청발생 → 리소스 낭비 발생
- WebSocket(server push)
실시간 양방향 통신을 위한 스펙, 서버와 브라우저가 지속적으로 연결된 TCP 라인을 통해 실시간으로 데이터를 주고받는 HTML5 사양 → 연결지향 양방향 전이중 통신이 가능 (채팅, 게임 주식, 차트등에 사용된다) → 서버 클라이언트간 양방향 통신
- Server Sent Event(SSE)
이벤트가 (서버 → 클라이언트) 방향으로만 흐르는 단방향 통신 채널 http 요청을 보낼 필요 없이 http 연결을 통해 서버에서 클라이언트로 데이터를 보낼 수 있다.
Server Sent Event(SSE)
- HTTP 스트리밍을 통해 서버에서 클라이언트로 단방향의 Push Notification을 전송할 수 있는 HTML5 표준 기술
- EventStream 최대 개수는 HTTP/1.1 6개, HTTP/2 사용시 최대 100개까지 유지 가능 합니다.
- Javascript의 EventSource를 이용하여 사용 가능 → 접속에 문제가 있으면 자동으로 연결을 재시도 하는 특징이 있습니다.
- IE 에서 polyfill을 통해 사용가능 → 이제는 보내줍시다...
- Server To Client 단반향 연결 지원 → 서버에서 데이터를 프론트로 일방적으로 내려주는 케이스에서 적절
Spring 에서 SSE를 적용 해보자
적용전 해줘야 할는게 있는데 바로 Postman 과 VsCode 를 설치 해야 합니다 → Data 전송 과 화면 표시를 위해
- VsCode Extension LiveServer 설치 → html 파일을 바로 로드하여 확인하기 위해
- PostMan → 서버 Event 발생 도구
- Spring → web, lombock, devtool (dependecy 설정)
요구 사항
- 화면
- 데이터를 화면에서 실시간으로 표시
- [ Title ] --> [ Text ]
- 서버 컨트롤러
- Cros / GetMapping → SseEmitter 를 이용 (서버 → 클라이언트) 단방향 통신 대기
- Cros / PostMapping(ToALL) → 연결 되어 있는
먼저 코드를 적어놓고 하나씩 분석해 가면서 설명 하겠습니다.
VsCode - sseIndex.html
VsCode 에 임의로 html 파일을 만들어 표시되는걸 보기위한것 → 간단히 작성할 예정
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script> <title>Document</title> </head> <body> <input id="userId" disabled="false"> <ol id="list"> </ol> <script> $(document).ready(function () { let userId = Math.floor((Math.random() * 1000) + 1); document.getElementById("userId").value = userId; // LiveServer 에서 사용하는 userId let urlENdPoint = 'http://localhost:8080/subscribe?userId=' + userId; let eventSource = new EventSource(urlENdPoint); // javascript SSE 받아서 사용하는 부분 eventSource.addEventListener("latestNews", function (event) { console.log("urlEndPoint : " + urlENdPoint) console.log("EVENT : " + event); console.log("EVENT : " + event.data); let artilceSource = JSON.parse(event.data); console.log("artilceSource : "+ artilceSource) $("#list").append("<li>" + artilceSource.Title +" --> "+ artilceSource.Text + "</li>"); }); }) </script> </body> </html>
NewsController.java
package com.example.RealTimeNews.controller; // package 위치 -> Controller 형태로 받을 예정 import lombok.extern.slf4j.Slf4j; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; @RestController @Slf4j public class NewsController { public List<SseEmitter> emitters = new CopyOnWriteArrayList<>(); public Map<String, SseEmitter> mapEmitters = new ConcurrentHashMap<>(); // method for client subscription @CrossOrigin @GetMapping(value = "/subscribe", consumes = MediaType.ALL_VALUE) public SseEmitter subscribe(@RequestParam String userId) { SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE); sendInitEvent(sseEmitter); logFormListEmitters(sseEmitter); logFormMapEmitters(userId, sseEmitter); return sseEmitter; } // method for dispatching events to all clients @CrossOrigin @PostMapping(value = "/dispatchEventToAll") public void dispatchEventToClients(@RequestParam String title, @RequestParam String text){ Map<String, String> mapForJson = Map.of("Title", title, "Text", text); for (SseEmitter emitter : emitters) { try { emitter.send(SseEmitter.event().name("latestNews").data(mapForJson)); log.info("PostMapping for all clients SSE Emitters : {}", emitters); } catch (IOException e) { emitters.remove(emitter); log.info("Exception SSE Emitters : {}", emitters); } } } // method for dispatching events to specific clients @CrossOrigin @PostMapping(value = "/dispatchEventToSpecific") public void dispatchEventToSpecificClients(@RequestParam String title, @RequestParam String text, @RequestParam String userId){ Map<String, String> mapForJson = Map.of("Title", title, "Text", text); SseEmitter sseEmitter = mapEmitters.get(userId); if (sseEmitter != null) { try { sseEmitter.send(SseEmitter.event().name("latestNews").data(mapForJson)); log.info("PostMapping for specific clients SSE Emitters : {}", mapEmitters); } catch (IOException e) { // emitters.remove(sseEmitter); mapEmitters.remove(userId); log.info("Exception SSE Emitters : {}", mapEmitters); } } } private void sendInitEvent(SseEmitter sseEmitter) { try { sseEmitter.send(SseEmitter.event().name("INIT")); } catch (IOException e) { e.printStackTrace(); } } private void logFormListEmitters(SseEmitter sseEmitter) { log.info("------------------All Clients-----------------"); log.info("first SSE Emitters : {}", emitters); sseEmitter.onCompletion(() -> emitters.remove(sseEmitter)); log.info("onCompletion remove SSE Emitters : {}", emitters); emitters.add(sseEmitter); log.info("after add SSE Emitters : {}", emitters); } private void logFormMapEmitters(String userId, SseEmitter sseEmitter) { log.info("------------------SpecificClients-----------------"); log.info("first SSE Emitters : {}", mapEmitters); mapEmitters.put(userId, sseEmitter); log.info("map SSE Emitters : {}", mapEmitters); sseEmitter.onCompletion(() -> mapEmitters.remove(userId)); log.info("onCompletion remove SSE Emitters : {}", mapEmitters); sseEmitter.onTimeout(() -> mapEmitters.remove(userId)); log.info("onTimeout remove SSE Emitters : {}", mapEmitters); sseEmitter.onError((e) -> mapEmitters.remove(userId)); log.info("onError remove SSE Emitters : {}", mapEmitters); } }
PostMan 데이터
http://localhost:8080/dispatchEventToAll?title=Corona is ending&text=I am Happy
- Params / key : title | value : Corona is ending
- Params / key : text | value : I am Happy
http://localhost:8080/dispatchEventToSpecific?title=only this id sent&text=hello!!!&userId=853
- Params / key : title | value : only this id sent
- Params / key : text | value : hello!!!
- Params / key : userId | value : 853 (VsCode 에 있는 userId)
참조
Uploaded by Notion2Tistory v1.1.0