사내 프로젝트를 진행하며 실시간 이벤트를 수신하여 즉시 웹 페이지에 알림 팝업을 띄우고 요소를 추가하라는 업무가 주어졌다. 처음엔 당연히 웹 소켓으로 구현을 하려다가 시간이 충분하지 않다는 것을 알게 되어 폴링 방식을 고려했다가 성능 저하 및 자원 낭비라고 생각되어 이름만 알고 있던 SSE를 찾아보았다.
이벤트를 내려주기만 하는 단방향 구조이며, 모든 로직은 JS(프론트)단에서 이루어지기 때문에 SSE 방식을 선택하게 되었다.
Socket | SSE(Server-Sent-Event) | |
---|---|---|
브라우저 지원 | 대부분 브라우저에서 지원 | 대부분 모던 브라우저 지원(polyfills 가능) |
통신 방향 | 양방향 | 일방향(서버에서 클라이언트로) |
리얼타임 | Yes | Yes |
데이터 형태 | Binary, UTF-8 | UTF-8 |
자동 재접속 | No | Yes(3초마다 제시도) |
최대 동시 접속 수 | 브라우저 연결 한도는 없지만 서버 셋업에 따라 다름 | HTTP를 통해서 할 때는 브라우저당 6개 까지 가능 / HTTP2로는 100개가 기본 |
프로토콜 | websocket | HTTP |
베터리 소모량 | 큼 | 작음 |
Firewall 친화적 | Nope | Yes |
가장 중요하게 볼 것은 통신 방향이다.
소켓은 양방향 통신으로 서로의 연결을 끊지 않고 맞잡을 형태로 구독을 하고 있으며 구독이 끊길 경우 자동 재접속이 되지 않는다. (라이브러리 등을 사용하면 처리 가능하다.)
SSE는 단방향 통신으로 첫구독 요청 이후 클라이언트에서는 서버로 데이터 전송을 하지 않는다. 데이터를 수신만 할 수 있으며 xml, json 형태 등으로 받을 수 있다.
package com.example.demo;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Map;
/**
* SseController.java
* Class 설명을 작성하세요.
*
* @author 이유나
* @since 2023.03.16
*/
@RequiredArgsConstructor
@RestController
public class SseController {
private final SseService sseService;
/**
* SSE 최초 접속
* @param jwt
* @return SseEmitter
*/
@GetMapping("/sse/realtimeEvnt")
public SseEmitter subscribe(String jwt) {
SseEmitter sseEmitter = new SseEmitter();
try {
sseService.subscript(jwt, sseEmitter);
} catch (IOException exception) {
throw new RuntimeException("연결 오류!");
}
return sseEmitter;
}
/**
* 데이터 수신 및 SSE 송신
* @param param
* @throws IOException
* @return ResponseEntity
*/
@PostMapping("/sse/send")
public ResponseEntity<String> send(@RequestBody Map<String, Object> param) throws IOException {
// sse 이벤트 발송
ObjectMapper objectMapper = new ObjectMapper();
sseService.send(objectMapper.writeValueAsString(param));
return ResponseEntity.ok().body("OK");
}
}
package com.example.demo;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* SseService.java
* Class 설명을 작성하세요.
*
* @author 이유나
* @since 2023.03.16
*/
@Service
public class SseService {
private final Map<String, SseEmitter> cluents = new ConcurrentHashMap<>();
/**
* SSE 최초 접속
*
* @param jwt
* @param sseEmitter
* @return SseEmitter
* @throws IOException
*/
public SseEmitter subscript(String jwt, SseEmitter sseEmitter) throws IOException {
sseEmitter.send(SseEmitter.event().data("{\"msg\": \"SSE CONNECT\"}"));
cluents.put(jwt, sseEmitter);
sseEmitter.onTimeout(() -> cluents.remove(jwt));
sseEmitter.onCompletion(() -> cluents.remove(jwt));
return sseEmitter;
}
/**
* 데이터 수신 및 SSE 송신
*
* @param jsonStr
* @throws IOException
*/
public void send(String jsonStr) throws IOException {
Set<String> deadIds = new HashSet<>();
cluents.forEach((id, emitter) -> {
try {
emitter.send(jsonStr, MediaType.APPLICATION_JSON);
} catch (Exception e) {
deadIds.add(id);
System.out.println("disconnected id :" + id);
}
});
deadIds.forEach(cluents::remove);
}
}
//sse이벤트 수신
const eventSource2 = new EventSource(`/sse/realtimeEvnt?jwt=${Math.random()}`);
eventSource2.onopen = (e) => {
console.log(e.data);
};
eventSource2.onerror = (e) => {
console.log("=====수신실패=====")
};
eventSource2.onmessage = (e) => {
console.log(e.data)
}
SSE는 서버에서 SSE를 구독하고 있는 클라이언트를 순회하며 이벤트를 쏘고, 기본 접속 수가 적어 성능 저하가 올 수 있다. 하지만 사내 프로젝트에서는 클라이언트가 5대가 채 되지 않는 환경으로 사용할 수 있었다.
클라이언트의 수가 어떻든 부하 성능이 대응 가능하도록 더 많이 공부해야겠다.
참고
[SERVER][HTTP, AJAX 통신, WebSocket, SSE] 특징, 장단점 — 디벨로폴리 (tistory.com)
웹소켓 과 SSE(Server-Sent-Event) 차이점 알아보고 사용해보기 — 개발자로 살아남기-캐나다 (tistory.com)