SSE 는 그냥 단순하게 단향방 통신에서 사용된다 이정도만 알고 사용하였다.
그저 HTTP에서 지원해는 기능? 정도 구나라고 생각했지만
HTTP가 지원하는 기능이라기보다는
HTTP 위에 정의된 이벤트 전송 규약(HTML5 표준)이라고 한다.
SSE는 HTTP 위에서 동작하는 표준화된 이벤트 스트리밍 방식이다.
그래서 별도의 라이브러리 없이 사용할 수 있나보다....
const eventSource = new EventSource("/subscribe");
GET /subscribe HTTP/1.1
Host: api.example.com
Accept: text/event-stream
브라우저는 /subscribe에 HTTP GET 요청을 보낸다.
client 가 보내는 단순 GET 요청에 Accept: text/event-stream 헤더 포함만 하는것이다. (단순 요청)
@GetMapping(value = "/api/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@ResponseStatus(HttpStatus.OK)
public SseEmitter subscribe(@AuthenticationPrincipal UserDetails userDetails,
// Last-Event-ID 헤더는 마지막으로 받은 이벤트부터 이벤트 스트리밍을 재개하는 데 사용됩니다.
@RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "")
String lastEventId) {
return sseNotificationService.subscribe(userDetails.getUsername(), lastEventId);
}
HTTP/1.1 200 OK
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache
Connection: keep-alive
Transfer-Encoding: chunked
이때 컨트롤러가 SseEmitter를 반환하면
Spring MVC가 HTTP 응답을 끊지 않고 열린 스트림으로 바꿔줍니다.
여기서 궁금한게 컨트롤러에서 단순하게 SSeEmitter 를 반환했을 뿐인데
헤더에 Content-Type: text/event-stream을 반환 하는것일까 ?
1. DispatcherServlet
// DispatcherServlet 내부
returnValueHandler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
2. Handler 선택
SseEmitter는 ResponseBodyEmitter를 상속ResponseBodyEmitterReturnValueHandler가 선택됨public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType());
}
}
3. ResponseBodyEmitterReturnValueHandler 실행
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
mavContainer.setRequestHandled(true); // DispatcherServlet이 view 렌더링 안 함
HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
response.setHeader("Cache-Control", "no-store");
response.setContentType("text/event-stream;charset=UTF-8"); // 🔹 여기서 헤더 세팅
ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue;
emitter.initialize(response); // Emitter와 응답 OutputStream 연결
}
4. Emitter 동작
이제 컨트롤러에서 반환한 SseEmitter 객체는 응답 OutputStream에 바인딩됨
이후 다른 스레드에서 emitter.send(...) 하면 → response stream에 바로 쓰여 나갑니다.
HTTP 응답은 200 OK 상태로 열려 있고, 끊어지지 않음
SseEmitter.send()가 내부적으로 ServletResponse.getOutputStream().write(...) 까지 이어지는 실제 I/O 플로우는 나중에 자바 입출력을 공부하고 복습해보자.
이제 이벤트가 발생했을때 (emitter.send를 했을때) Client 에 어떤 형태로 응답이 보내질까?
SSE 는 서버 -> Client 니까 매번 새로운 HTTP 응답이 발생하는것인줄 알았다.
하지만 그게 아니라 최초의 응답의 헤더를 그대로 사용하는것이였다.
HTTP/1.1 200 OK
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache
Connection: keep-alive
Transfer-Encoding: chunked
응답 body (계속 append 됨)
data: hello world
id: 1
event: notification
data: {"msg":"새 알림"}
id: 2
data: ping
정리하면
헤더는 최초 한 번 내려가고 그 상태에서 연결 유지됨.
이벤트가 생길 때마다 body에 줄 단위로 append.
클라이언트(EventSource)는 이 body 스트림을 읽어서 이벤트로 파싱.
즉, emitter 객체가 내부적으로 response outputStream을 가지고 있고, 그걸 통해 write 가능한 상태가 됩니다.
outputStream.write("data: ...\n\n".getBytes());
outputStream.flush();
즉, 호출 즉시 응답 body에 write + flush가 일어납니다.
메서드 종료와 무관
일반적인 @ResponseBody 응답은 메서드가 return 할 때 한 번에 body를 만들어 내려주죠.
SSE는 다릅니다.
SseEmitter가 응답을 열어둔 상태라, 메서드가 끝났든 말든 send() 하는 순간 body에 즉시 append됩니다.
1.SseEmitter.send() 호출
2.응답 body는 이미 열려 있는 HTTP 스트림 (Content-Type: text/event-stream)
3.네트워크 전송
4.클라이언트 쪽 이벤트 발생