🔈 알림 기능 구현 방법 4가지

Short-Polling
- 클라이언트가 서버에 짧은 주기를 두고 정기적으로 새로운 이벤트가 있는지 요청을 보내는 방법
- 서버에서 보내줄 새로운 데이터가 없어도 즉시 빈 응답을 보내고 받는 작업을 반복하기 때문에 불필요한 트래픽 발생, 비효율적
Long-Polling
- Short Polling과 달리 새로운 이벤트가 발생하면 응답하는 방법
- 불필요한 요청과 응답이 없어지고 보다 실시간성이 보장됨
- 그러나 커넥션을 유지하는 동안은 서버의 리소스 지속적으로 소모
- 응답이 없다면 timeout (보통 100~300 seconds로 설정)
SSE(Server Sent Events)
- 클라이언트가 HTTP Request 헤더로 keep-alive 요청을 하면 일정 시간 동안 연결이 유지됨
- 연결 유지되는 동안 HTTP 스트리밍을 통해 (서버→클라이언트) 단방향으로만 push notification 전송
- Spring Framework 버전 4.2부터 SseEmitter 클래스로, 버전 5부터는 WebFlux로 SSE 구현 가능
Web Socket
- handshake로 연결된 후 ws 프로토콜로 전환되어 메시지를 주고받음
- 서버가 terminate를 보낼 때까지 연결이 지속됨
- 실시간 양방향 통신 가능
- HTTP를 사용하지 않으므로 별도의 라이브러리를 사용해야 함
💭 어떤 방법이 적절한가?
SSE를 선택
- Polling 방식은 요청을 보내야 응답을 받을 수 있기 때문에 불필요한 트래픽 발생
- Web Socket 방식은 HTTP를 사용하지 않아 학습곡선이 요구되고, 양방향 통신은 필요하지 않음
- 현재 Spring Framework를 사용하는 중이고 서버→클라이언트 단방향 전송만 필요하므로 SSE를 선택
🤚🏼 단, 고려 사항
- SSE는 브라우저 마다 도메인당 허용되는 HTTP 연결 수가 제한되어있음 (HTTP/2를 사용한다면 도메인 당 최대 100개의 연결 가능)
→ 주로 소규모 연결이 보장되는 경우에 SSE를 사용한다고 함 (예: 파일 다운 진행률 표시) - 때문에 단방향 알림은 Polling을 쓰기도 하는데, 프로젝트 규모가 작기도 하고 한 사람이 브라우저 당 탭을 100개 이상 열지 않는 한 문제가 되지 않을 것 같아서 사용하기로 함
🙋🏻♀️ SSE 통신 구현 방법
1. 클라이언트가 서버에게 SSE 통신 연결 요청
클라이언트가 서버에게 SSE 연결 요청을 할 때 일반적으로 EventSource라는 API를 사용한다.
//사용자 id 확인
let userId = document.getElementById("myName").innerText;
console.log(userId);
if(userId.length > 0){
//SSE 연결
const eventSource = new EventSource("/sse" + "?userId=" + userId);
//사용자에게 알림 팝업 표시
eventSource.addEventListener("alarm", function(event){
let message = event.data;
Swal.fire({
position: 'top-end',
icon: 'success',
title: message,
showConfirmButton: false,
timer: 1500
});
})
💡 EventSource
SSE 연결 요청 시 client-side에서 사용되는 Web API이다.
const eventSource = new EventSource("/sse");
EventSource 객체를 생성할 때 특정 url을 지정하면 해당 url에서 발생한 이벤트가 응답으로 온다.
event: alarm
data: 새로운 알림이 있습니다!
id: user123
(서버 응답 형식은 위와 같다.)
eventSource.onmessage = (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
newElement.textContent = `message: ${event.data}`;
eventList.appendChild(newElement);
};
응답이 올 때마다 onmessage()라는 event handler가 실행된다.
위 코드는 새로운
- 요소를 하나 생성하고 서버에서 받은 응답의 data를 꺼내 작성하고 있다.
eventSource.addEventListener("ping", (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
const time = JSON.parse(event.data).time;
newElement.textContent = `ping at ${time}`;
eventList.appendChild(newElement);
});
응답의 event필드에 의해 addEventListener()라는 event listener가 실행될 수 있다.
응답의 event필드 값이 'event: ping'이라면 위 event listener가 동작하여 콜백함수를 실행한다.
위 콜백 함수는 event.data를 JSON으로 파싱한 후 화면에 출력하고 있다.
[Exposed=(Window,Worker)]
interface EventSource : EventTarget {
constructor(USVString url, optional EventSourceInit eventSourceInitDict = {});
readonly attribute USVString url;
readonly attribute boolean withCredentials;
// ready state
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSED = 2;
readonly attribute unsigned short readyState;
// networking
attribute EventHandler onopen;
attribute EventHandler onmessage;
attribute EventHandler onerror;
undefined close();
};
dictionary EventSourceInit {
boolean withCredentials = false;
};
EventSource 인터페이스를 확인해보면 위 내용이 구현되어있다.
- EventTraget을 상속받고 있음
- 생성자 파라미터로 url을 지정해주어야 함
- connecting, open, closed 상태가 존재
- onopen, onmessage, onerror라는 event handler 존재
이외에 EventSource에 대한 내용
- Get요청을 한다.
- 연결이 끊기면 클라이언트가 자동으로 재요청한다. (HTTP 301, 307 redirect)
- 단 HTTP 204 No Content response code가 오면 재요청을 하지 않는다.
- SSE 연결 요청 시 header을 수정할 수 없다.
(EventSource의 더 자세한 내용은 HTML Standard 문서 참조)
2. 서버의 응답
SSE 연결을 관리하는 Spring 객체 SseEmitter를 사용하여 서버에서 클라이언트로 응답을 보내보자.
@RestController
@RequestMapping("/sse")
@Slf4j
public class SseController {
//멀티 스레드 동시성 보장
public static Map<String, SseEmitter> sseEmitters = new ConcurrentHashMap<>();
@GetMapping(value = "", consumes = MediaType.ALL_VALUE)
public SseEmitter streamSseMvc(@RequestParam String userId) {
log.info("userId = {}", userId);
//현재 클라이언트를 위한 SseEmitter 생성
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
try {
//연결
emitter.send(SseEmitter.event().name("connect"));
} catch (IOException e) {
e.printStackTrace();
}
//userId를 key값으로 SseEmitter를 저장
sseEmitters.put(userId, emitter);
emitter.onCompletion(() -> sseEmitters.remove(userId));
emitter.onTimeout(() -> sseEmitters.remove(userId));
emitter.onError((e) -> sseEmitters.remove(userId));
return emitter;
}
}
클라이언트가 보낸 Get요청을 처리하는 메서드이다. 파라미터로 쿼리 스트링으로 전달된 userId를 받는다.
💡 SseEmitter
SSE 응답을 보내기 위해 server-side에서 사용되는 API이다.
SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
SseEmitter 객체를 생성할 때 Long타입으로 timeout value(단위:milliseconds)를 지정해준다.
timeout을 지정하지 않으면 서버의 디폴트 시간으로 설정된다.
emitter.send(event().data(myObject));
send()를 사용해 클라이언트에게 이벤트를 보낸다.
* references
- https://jisu-y.github.io/cs/network-web-socket/
- https://velog.io/@wnguswn7/Project-SseEmitter로-알림-기능-구현하기
- https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
- https://developer.mozilla.org/en-US/docs/Web/API/EventSource
- https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.html
- https://velog.io/@lucky-jun/Server-Sent-events-with-EventSource
'CS' 카테고리의 다른 글
| [디자인 패턴] 빌더패턴 (Builder Pattern) (0) | 2025.01.10 |
|---|---|
| [디자인 패턴] 정적 팩토리 메서드 (Static Factory Method) (0) | 2025.01.08 |
| [운영체제] 동시성 이슈 해결 방법 - Semaphore/Mutex (8) | 2024.12.31 |
| [운영체제] 동시성 이슈란 - Race Condition/Data Race (1) | 2024.12.23 |
| [운영체제] 프로세스 - 메모리 공간/상태(state)/문맥 교환(context-switching)/pcb/프로세스 vs 스레드 (0) | 2024.12.20 |