우리는 실시간알림이 필요했고 FCM 방식과 SSE 방식을 고민했다.
둘다 실시간 알림을 전달하는 데 사용할 수 있는 기능
FCM은 Firebase SDK를 통해 작동 -> 추가 라이브러리 설정 및 Firebase 서비스에 의존
SSE는 웹 브라우저에서 기본 지원으로 추가 라이브러리 없이 쉽게 구현 가능
또한, PWA를 사용했기 때문에 App보다는 Web 환경에 어울리는 기술을 선택하는 것이 맞다고 생각하여, SpringBoot를 사용해서 SSE 기술로 실시간 알림을 구현
public interface NotificationRepository extends JpaRepository<Notification, Long> {
boolean existsByReceiverIdAndRead(Long userId, boolean isRead);
}
@Override
public boolean getStatus(Long userId) {
return notificationRepository.existsByReceiverIdAndRead(userId, false);
}
실시간으로 SSE 처리하기위해서 메인 스레드가 아닌 별도의 스레드에서 주기적인 작업을 수행하도록 설정
덕분에 클라이언트와의 연결을 유지하면서 다른 요청을 처리할 수 있음
단일 스레드 :
다중 스레드 :
따라서, 현재 웹앱 서비스는 적은 수의 클라이언트를 보유하기에 단일 스레드로 진행하고 추후 클라이언트 수가 증가한다면 다중으로 리팩토링하기로 결정
@GetMapping(value = "/status", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleSse() {
// 무제한 타임아웃 설정을 가진 SseEmitter 객체 생성
SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
// 단일 스레드로 작업을 스케줄링 할 수 있는 ScheduledExecutorService 생성
ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
// 5초마다 주기적으로 작업을 수행하도록 설정
executor.scheduleAtFixedRate(() -> {
try {
// 사용자 상태를 가져와서 "1" 또는 "0"으로 변환
String result = notificationService.getStatus(UserHeaderInterceptor.userId.get()) ? "1" : "0";
// 클라이언트에 결과 전송
sseEmitter.send(result);
// 로그에 전송된 정보 기록
log.info("정보를 한번 보내볼게요 : " + result);
} catch (IOException e) {
// IOException 발생 시, SSE 연결 종료 처리
log.info("정보 보내는 거 종료1");
sseEmitter.completeWithError(e); // 오류와 함께 SSE 완료
executor.shutdown(); // 스케줄러 종료
}
}, 0, 5, TimeUnit.SECONDS); // 0초 후 시작하고, 5초 간격으로 반복
// SSE가 완료되면 스케줄러 종료
sseEmitter.onCompletion(executor::shutdown);
// SSE가 타임아웃되면 스케줄러 종료
sseEmitter.onTimeout(executor::shutdown);
// 로그에 SSE 시작 메시지 기록
log.info("정보 보내는 거 종료2");
// SseEmitter 반환하여 클라이언트 연결을 유지
return sseEmitter;
}
@GetMapping(value = "/status", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handleSse() {
// 무제한 타임아웃 설정을 가진 SseEmitter 객체 생성
SseEmitter sseEmitter = new SseEmitter(Long.MAX_VALUE);
// 스레드 풀 생성 (예: 최대 5개의 스레드)
ScheduledExecutorService executor = Executors.newFixedThreadPool(5);
// 5초마다 주기적으로 작업을 수행하도록 설정
executor.scheduleAtFixedRate(() -> {
try {
// 사용자 상태를 가져와서 "1" 또는 "0"으로 변환
String result = notificationService.getStatus(UserHeaderInterceptor.userId.get()) ? "1" : "0";
// 클라이언트에 결과 전송
sseEmitter.send(result);
// 로그에 전송된 정보 기록
log.info("정보를 한번 보내볼게요 : " + result);
} catch (IOException e) {
// IOException 발생 시, SSE 연결 종료 처리
log.info("정보 보내는 거 종료1");
sseEmitter.completeWithError(e); // 오류와 함께 SSE 완료
executor.shutdown(); // 스케줄러 종료
}
}, 0, 5, TimeUnit.SECONDS); // 0초 후 시작하고, 5초 간격으로 반복
// SSE가 완료되면 스케줄러 종료
sseEmitter.onCompletion(() -> {
executor.shutdown(); // SSE 완료 시 스케줄러 종료
log.info("SSE 연결이 종료되었습니다.");
});
// SSE가 타임아웃되면 스케줄러 종료
sseEmitter.onTimeout(() -> {
executor.shutdown(); // 타임아웃 시 스케줄러 종료
log.info("SSE 연결이 타임아웃되었습니다.");
});
// 로그에 SSE 시작 메시지 기록
log.info("정보 보내는 거 시작");
// SseEmitter 반환하여 클라이언트 연결을 유지
return sseEmitter;
}
<script>
const eventSource = new EventSource('https://j11a604.p.ssafy.io/api/notifications/status');
eventSource.onmessage = function(event) {
const eventsDiv = document.getElementById('events');
eventsDiv.innerHTML += `<p>${event.data}</p>`;
};
eventSource.onerror = function(event) {
console.error("Error occurred:", event);
eventSource.close();
};
</script>
읽지않은 메일이 있다면 "1", 없다면 "0" 이 event.data에 저장