SSE 적용기
도입 계기
현재 진행 중인 관제 플랫폼 서비스에서는 특정 차량의 총 주행거리가 5,000km 이상일 경우 차량 점검 대상임을 판단하고, 해당 차량의 점검 필요 알림을 사용자에게 전달하는 기능을 구현이 필요 했다. 이 알림 기능의 경우 서버에서 클라이언트로의 일방향 전송만 필요하기 때문에, 웹소켓(양방향 통신) 대신 SSE(Server-Sent Events)를 사용하는 방안으로 진행하기로 했다.
SSE 장점과 단점
SSE의 장점
- 서버에서 클라이언트로의 단방향 스트리밍이므로 구현 및 유지보수가 비교적 간단하다.
- 웹소켓보다 오버헤드가 적어, 단순 알림 전송에 적합하다.
- 기본적으로 클라이언트에서 연결이 끊어졌을 때 자동으로 재연결을 시도한다.
SSE의 단점
- 클라이언트에서 서버로의 실시간 데이터 전송이 필요할 경우에는 별도의 API 호출이 필요하다.
- 브라우저 지원 제한: 일부 구형 브라우저에서는 지원되지 않을 수 있으므로, 폴리필(polyfill) 적용이 필요할 수 있다.
- HTTP 헤더를 통한 커스터마이징에 제한이 있을 수 있으며, 네트워크 프록시 환경에서 문제를 발생할 수 있다.
- 프록시 서버가 높은 수의 지속적인 연결을 처리 시 성능 저하나 네트워크 지연이 발생할 수 있다. 이로 인해 클라이언트에게 실시간 데이터가 지연되거나 연결이 끊어지는 현상이 발생할 수 있다.
SSE 적용 코드
컨트롤러 설정
// AlarmController.java
@GetMapping("/subscribe")
@PreAuthorize("hasRole('ROLE_USER')")
public SseEmitter subscribe(@AuthenticationPrincipal CustomUserDetails user) {
return alarmService.subscribe(user.getId());
}
- 위 코드는 로그인한 사용자가 알림 구독 요청을 보낼 때, SseEmitter를 반환하여 SSE 연결을 생성한다.
서비스 로직
// AlarmService.java
public SseEmitter subscribe(String userId) {
SseEmitter sseEmitter = new SseEmitter(0L);
sseEmitters.put(userId, sseEmitter);
sseEmitter.onCompletion(() -> sseEmitters.remove(userId));
sseEmitter.onTimeout(() -> sseEmitters.remove(userId));
sseEmitter.onError(event -> sseEmitters.remove(userId));
return sseEmitter;
}
- 해당 메서드는 사용자별로 SseEmitter를 관리하며, 연결 종료, 타임아웃, 에러 발생 시 자동으로 해당 사용자의 연결을 제거한다
알림 전송
public void sendAll(SendAlarm sendAlarm) {
sseEmitters.forEach((userId, emitter) -> {
try {
emitter.send(
SseEmitter.event()
.name(String.valueOf(sendAlarm.getStatus()))
.data(sendAlarm, MediaType.APPLICATION_JSON)
);
} catch (IOException e) {
logger.error(userId + " 메세지 전송 중 문제 발생", e);
sseEmitters.remove(userId);
}
});
}
알림 상태를 이벤트 이름으로 설정하여, 클라이언트에서는 해당 상태에 따라 다른 방식으로 알림을 표시할 수 있다. 모든 연결된 사용자에게 알림을 전송하며, 전송 중 오류가 발생하면 해당 사용자의 SseEmitter를 제거할 수 있다.
적용 결과
알림 전송 후, 클라이언트에서 아래 이미지와 같이 정상적으로 알림을 확인이 가능했다!