Spring 에서 Server-Sent Events(SSE)로 브라우저 알림 구현하기
ServerSideEvent 로 사용자 브라우저에 알림 주기
채팅 기능이 생기면서 실시간으로 사용자에게 알림을 주고 싶은 일이 생겼다. 브라우저 알림을 주려고 하는데 이는 Server Side Event 로 구현이 가능했다.
ServerSideEvent 가 뭐지 ?
Server Side Event 는 서버에서 클라이언트엑 통신을 하는 방법이라고 할 수 있다. Websocket 은 서버와 클라이언트 사이에 왔다갔다 할 수 있는 양방향이었다면 SSE는 단방향이다! 단방향인 만큼 전력 소비량도 적다. 그리고 http 프로토콜을 사용해서 에러 처리에도 굉장히 용이하다.
Server Side Event 를 보내보자!
우선 client 에서 server 에 요청을 보내서 나만의 sseEmitter 를 저장하도록 해야한다. 따라서 SseController.java 를 사용자 id 기반으로 1개씩 저장 할 수 있도록 만들어 주었다.
1
2
3
4
5
6
7
8
9
// SseController.java
@GetMapping(value="/connect/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> (HttpServletRequest res, @PathVariable String id) {
SseEmitter emitter = sseEmitter.getEmmiterById(id);
if (emitter != null) return ResponseEntity.ok(emitter);
emitter = new SseEmitter(60 * 60 * 1000L);
sseEmitters.add(id, emitter);
return ResponseEntity.ok(emitter);
}
그리고 Client 단에서 아래와 같이 요청하면 sseEmitter 를 받을 수 있다.
1
2
//js
const sse = new EventSource("/connect/12345")
SseEmitter 에 대한 class는 따로 만들어 주었다. 아래처럼 만들어 Map 에 저장하고 user 의 id 가 들어오면 저장하고 있던 sseEmitter 를 return 할 수 있도록 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//SseEmitters.java
public class SseEmitters {
private Map<String, SseEmitter> CLIENTS;
public void add (String id, SseEmitter sseEmitter){
CLIENTS.put(id, sseEmitter);
sseEmitter.onCompletion(() -> {
if(CLIENTS.containsKey(id)) {
SseEmitter sseEmitter = CLITENTS.remove(id);
sseEmitter = null;
} // sseEmitter 가 없어 질땐 삭제한다. 11월 27일 Mem Leak 이 해결된 코드이다!
});
sseEmitter.onTimeout(() -> {
sseEmitter.complete();
});
return sseEmitter;
}
public getEmitterById (String id) {
if(CLIENTS.containsKey(id)) {
return CLIENTS.get(id);
}
return null;
}
public removeEmitterById(String id) {
if(CLIENTS.containsKey(id)) {
SseEmitter sseEmitter = CLIENTS.remove(id);
sseEmitter = null;
} // 11월 27일 Mem Leak 이 해결된 코드이다!
}
}
브라우저 알림을 보내보자 !
이제 알림을 보내는 부분을 만들어보겠다!
1
2
3
4
5
6
public void sendBrowserAlarm(String participant, String body, String url, EventType type) {
Map<String, String> dataBody = new HashMap<>();
dataBody.put("body", body);
dataBody.put("url", url);
sseEmitters.publish(dataBody, participant);
}
1
2
3
4
5
6
7
8
9
10
//SseEmitters.java
public void publish(Map<String, String> dataBody, String targetId) {
SseEmitter targetEmitter = CLIENT.get(id);
if (targetEmitter == null) return;
try {
targetEmitter.send(SseEmitter.event().name("notify").data(dataBody));
} catch (Exception e) {
CLIENT.remove(id);
}
}
이렇게 하면 Client에게 notify 라는 Event 를 줄수가 있다. 그렇다면 Client 에서는 어떻게 받으면 될까 ?? 아까 client 단에서 받은 SseEmitter 에 event listener 를 달아주면 된다!
1
2
3
4
5
sse.addEventListener("notify", e => {
const {data: strDataBody} = e;
let dataBody = JSON.parse(strDataBody);
//Notification 코드 작성
})
Spring 에서 워낙 잘 만들어 놓은 까닭에 Sse Emitter 로 손쉽게 추가하여 Client 에 정보를 전달 할 수가 있었다!