SSE(Server Sent Events)란 서버 주체 단방향 통신 기술입니다.
이것이 무슨 말이냐 우리가 기본적으로 사용하는 일반적인 HTTP 통신은 Client가 요청을 보내면 서버는 해당 요청 건에 대해서만 응답을 할 수 있는 규칙을 가지고 있습니다.
이럴 경우 우리가 개발한 서버에서 어떤 작업이 이뤄졌을 때 Client가 알 수 있는 방법은 Client의 요청이 있을 때까지 기다리는 방법 밖에는 없습니다. 하지만 이렇게 구현을 하자면 Client는 일정 시간이 지날 때마다 서버를 조회하여 데이터를 최신화 시켜주는 방법 밖에 없을 것이고 이는 무분별한 호출로 인한 리소스 낭비로 인해 문제를 야기할 수도 있습니다.
그럼 서버에서 응답을 보내서 Client에서 처리하면 어떨까? 해서 나온 것이 웹소켓과 SSE기술입니다.
웹소켓의 경우 양방향 통신으로 handshaking방식으로 이루어집니다. 이는 Client와 서버의 통신이 연결되는 방식을 말하며 웹소켓에서 한번 연결된 통신은 지속적으로 유지되며, 이 통신을 통해 Client와 서버는 데이터를 주고 받을 수 있습니다. 이는 채팅과 같이 실시간 소통이 이뤄져야하는 기능을 구현 할 때 많이 사용되는 방법입니다.
하지만 알람과 같이 Client가 서버로 요청을 보내지 않아도 서버에서 특정 작업이 생겼을 때 Client가 알게 하고 싶다면 WebSocket방식은 과한 기능 구현인 것 같을 수 있습니다. 그럴때는 SSE를 적용하는 것을 고민해볼 수 있습니다.
SSE는 Client의 요청을 기다리지 않고 서버에서 Client로 요청을 보내는 기술로 서버에서 단방향 통신으로 Client로 응답을 보낼 수 있습니다. Client가 처음 한번의 요청을 통해 Client와 서버의 연결이 설정된 후 서버의 일방적인 데이터 전송이 가능해 집니다.
사용방법(Client - React)
Client의 설정은 간단합니다. 위에서 말했듯 Client에서의 작업은 초기 연결 설정을 위한 요청과 Event를 수신하여 작업할 부분만 작업해주면 됩니다.
useEffect(() => {
let eventSource: EventSource;
if (requestId.length) {
// 서버와 연결
eventSource = new EventSource(defaultURL + "/connect/" + requestId);
// 이벤트 수신 시 수행할 작업들
eventSource.addEventListener("message", handleEventMessage);
eventSource.addEventListener("error", handleEventError);
}
return (() => {
if (requestId) {
// 페이지가 종료될 때 설정한 이벤트들도 제거
eventSource.removeEventListener("message", handleEventMessage);
eventSource.removeEventListener("error", handleEventError);
}
})
}, [requestId]);
위 설정은 서버와의 초기 연결을 요청 보내는 부분입니다.
new EventSource()는 초기 서버 연결을 담당해주는 부분이며, 내부 파라미터로 url을 주입해주면 해당 URL로 Get요청을 보내줍니다.
이때 요청 Header의 Content-Type은 text/event-stream으로 지정되어 있습니다.
사용방법(Back-Spring)
서버는 Client보다 작업량 자체는 많지만 다른 웹소켓과 같은 기술들에 비하면 훨씬 적은 설정으로 구현할 수 있습니다.
우선 서버는 Client로부터 연결을 위한 요청이 들어온다면 HTTP Method와 produces즉 미디어 타입을 명시 해주어야합니다.
@GetMapping(value = "/connect/{requestId}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter connect(@PathVariable String requestId) {
// SseEmitter설정을 위해 sseService의 register을 사용
return sseService.register(requestId);
}
SseEmitter란?
SseEmitter란 Spring framework에서 sse기술을 적용해 관리하기 위하여 지원해주는 객체로써 해당 객체를 사용함으로써 개발자는 손쉽게 Sse기술을 적용할 수 있습니다.
해당 객체가 상속받은 ResponseBodyEmitter를 보면 하나 이상의 객체가 응답에 기록되는 비동기 요청을 처리하기 위한 컨트롤러 반환 타입이라고 명시가 되어있습니다.
이런 객체를 상속받아 구현된 객체인 만큼 비동기를 지원하며, Spring framework 4.2부터 사용가능합니다.
@Service
public class SseService {
//
private final long TIMEOUT_MILLISEC = 60L * 60 * 1000;
private final Map<String, SseEmitter> emitterMap = new ConcurrentHashMap<>();
public SseEmitter register(String requestId) {
SseEmitter emitter = new SseEmitter(TIMEOUT_MILLISEC);
config(emitter, requestId);
emitterMap.put(requestId, emitter);
return emitter;
}
private void config(SseEmitter emitter, String requestId) {
emitter.onCompletion(() -> emitterMap.remove(requestId));
emitter.onTimeout(() -> emitterMap.remove(requestId));
}
}
sseService의 register구현 부분입니다.
생성시 타임 아웃을 지정할 수 있으며, 해당 값은 연결된 이후 얼마나 시간을 유지 할 것인 지를 명시합니다.
해당 시간이 지나 요청을 할경우 onTimeout() 메소드가 자동합니다.
ConcurrentHashMap를 사용하여 inMemory로 관리를 하였으며, Thread safe인 자료구조를 사용하여 통신 연결이 MultiThread환경에서 공유되도록 구현하였습니다.
※ 주의
여러개의 Was환경을 구축한 경우 Redis를 활용하여 동기화되도록 구현 해야합니다.
위 설정을 통해 Client와 서버의 통신은 지속적으로 연결이 되며, 서버에서 client로 계속 데이터를 보낼 수 있는 환경이 구축 된 것 입니다.
Event send
서버에서 이벤트를 보내려면 기존 emitterMap에 저장해 놓은 sseEmitter를 사용하여 메시지를 보내게 됩니다.
public <E> void send(SseEmitter emitter, E data) {
try {
emitter.send(
SseEmitter
.event()
.name("message")
.data(data)
);
} catch (IOException e) {
emitter.completeWithError(e);
log.error(e.getMessage(), e);
}
}
emitter를 사용하여 send를 하게 되면 연결설정 되어있는 브라우저로 이벤트가 날라가게 되고,
위와 같이 지속적으로 웹브라우저 EventStream에 이벤트가 쌓이게 됩니다.
SseEmitter.event()에는 이벤트에 대해 설정을 할 수 있습니다.
- .id(): Event의 ID를 설정할 수 있습니다.
- .name(): Event의 Name을 설정할 수 있습니다. 이 이름은 Client에서 EventListener가 event를 식별하여 가져가는 기준이 됩니다.
- .data(): Event에 담겨있는 데이터입니다.
해당 통신을 종료하려면 complete()를 사용해야 하며, 통신 종료를 해주지 않으면 timeOut될 때까지 통신이 유지되어 메모리 누수 현상이 발생할 수도 있습니다.
사용시 주의
Sse는 Http1의 버전에서는 브라우저 6개에 한해서 지원해주는 기능입니다.
이는 브라우저 6개가 통신 연결이 되어있다면, 더이상의 연결이 불가한 사항인데, 이러한 점을 브라우저에서는 지원을 해주지 않고, 개선도 안되는 것으로 알고 있습니다.
하지만 Http2버전 부터는 100개까지 지원이 가능하다 하니 사용하시는 Http버전에 잘 맞춰 사용하시기 바랍니다.
2024.04.19 update
Font에서 EventSource 객체를 사용할 때 주의점
EventSource객체를 통해 통신 연결을 시도하게 되면 서버에서 complete를 하더라도 계속해서 연결을 시도하려합니다.
즉 작업이 끝나 통신을 서버에서 끊었음에도 API를 계속 호출하는 상황이 발생합니다.
이를 방지하기 위해 서버와 Client는 종료되는 부분을 명확하게 정하여 EventSource를 close해주시기 바랍니다.
'Server' 카테고리의 다른 글
[lombok]RequiredArgsConstructord와 Qualifier (0) | 2024.05.03 |
---|---|
[Spring] Bean은 어떻게 생성하고 주입할까? (0) | 2024.05.01 |
REST API란? (0) | 2024.04.14 |
[Docker] 도커로 데이터 베이스 편하게 사용하자~ (1) | 2024.04.08 |
[Docker] Docker 설치하기 (0) | 2024.04.08 |