안녕세계

[Spring] SSE란? (feat. SseEmitter) 본문

[Spring] SSE란? (feat. SseEmitter)

Junhong Kim 2024. 3. 3. 20:48
728x90
반응형

SSE(Server-Sent-Events)란?

웹 페이지가 새로운 데이터를 받기 위해서는 일반적으로 서버에 요청을 보내야 합니다. 그러나 SSE(Server-Sent Events)를 사용하면 서버가 요청 없이도 웹 페이지에 새로운 데이터를 전송할 수 있습니다. 이는 서버에서 클라이언트로 데이터를 주기적으로 전송해야 할 경우에 특히 유용합니다. 예를 들어, 서버가 특정 데이터의 변경 사항을 주기적으로 클라이언트에 알려주어야 하고, 클라이언트는 이를 대시보드 등의 화면에서 실시간으로 표시해야 하는 상황에서 활용될 수 있습니다.

 

웹소켓을 통한 구현도 가능하지만, 양방향 통신이 필요 없고 오직 서버에서 클라이언트로의 단방향 통신만 필요한 경우 SSE의 사용이 권장됩니다. SSE는 HTML5부터 지원되는 표준 스펙으로, 웹소켓이 연결을 HTTP로 시작한 뒤 자체 프로토콜로 통신하는 것과 달리, SSE는 전체 통신 과정을 HTTP를 통해 수행합니다.

서버로부터 이벤트 수신

SSE API는 EventSource 인터페이스에 포함되어 있습니다. 이벤트를 전달 받기 위해서 서버로 접속을 시작하려면 이벤트를 생성하는 서버측 스크립트를 URI로 지정하여 EventSource 객체를 생성합니다.

// 동일 도메인인 경우
const eventSource = new EventSource('/sse/v2');
  
// 다른 도메인인 경우
const eventSource = new EventSource('http://localhost:8080/sse/v2', {
  withCredentials: true,
});

EventSource 생성 후 message 이벤트에 대한 핸들러를 등록해서 서버로부터 전송된 메시지를 수신할 수 있습니다.

eventSource.onmessage = event => {
  const p = document.createElement("p");
  p.innerText = event.data;
  document.getElementById('result').appendChild(p);
};

또는 addEventListener()를 사용하여 이벤트를 기다릴 수 있습니다. 다음 예시 코드는 event 필드에 ping이 설정된 메시지가 서버로 부터 보내졌을 때만 호출된다는 점이 다릅니다.

eventSource.addEventListener('ping', event => {
  const p = document.createElement("p");
  p.innerText = event.data;
  document.getElementById('result').appendChild(p);
});

 

서버에서 이벤트 송신 (일반적인 사용 방법)

이벤트를 송신하는 서버는 MIME 타입으로 `text/event-stream`을 사용해 응답해야 합니다. 각 이벤트는 두 개의 줄바꿈(\n\n)으로 끝나는 텍스트 블럭으로 전송됩니다. 본 포스팅에서는 Spring 애플리케이션에서 이벤트를 송신하는 방법에 대해 알아보겠습니다. 다음 코드는 event 타입이 `ping`인 이벤트를 1초마다 랜덤 숫자를 송신합니다. 

@RestController
@RequestMapping("/sse")
class SseController {

    private val taskExecutor = Executors.newSingleThreadExecutor()

    @GetMapping(value = ["/v1"], produces = [MediaType.TEXT_EVENT_STREAM_VALUE])
    fun v1(response: HttpServletResponse) {
        response.setHeader("Cache-Control", "no-store")
        response.setHeader("Connection", "keep-alive")
        response.setHeader("Content-Type", MediaType.TEXT_EVENT_STREAM_VALUE)
        response.flushBuffer()
        for (i in 1..10) {
            // ping 이벤트를 1초마다 송신
            response.writer.write("event: pingping\n")
            // 랜덤 숫자 데이터 송신
            response.writer.write("data: ${Math.random()}\n\n")
            response.writer.flush()
            Thread.sleep(1000)
        }
    }
}

 

응답하는 데이터 형식은 `data:데이터\n\n` 입니다. 모든 작업이 끝난 후 response를 한번에 보내는 것이 아닌, 데이터 형식에 맞는 데이터가 write 될 때마다 해당 데이터를 클라이언트로 전송합니다.

서버에서 이벤트 송신(SseEmitter 사용)

Spring Farmework 4.2 부터는 SSE를 보다 쉽게 사용할 수 있는 구현체인 `SseEmitter()`를 제공합니다. 이 구현체를 사용하면 Content-Type, 데이터 포맷을 자동으로 맞춰주기 때문에 편리하게 사용할 수 있습니다. 다음은 SseEmitter를 사용한 SSE 사용 예입니다.

package com.example.expkopring.controller

import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter
import java.util.concurrent.Executors
import javax.servlet.http.HttpServletResponse

@RestController
@RequestMapping("/sse")
class SseController {

    private val taskExecutor = Executors.newSingleThreadExecutor()

    // ...

    @GetMapping("/v2")
    fun v2(response: HttpServletResponse): SseEmitter {
        val emitter = SseEmitter()
        taskExecutor.execute {
            try {
                for (i in 1..10) {
                    val event = SseEmitter.event()
                        .name("ping")
                        .data(Math.random())
                    emitter.send(event)
                    // emitter.send("event: ping\ndata: ${Math.random()}\n\n")
                    Thread.sleep(1000)
                }
                emitter.complete()
            } catch (e: Exception) {
                emitter.completeWithError(e)
            }
        }
        return emitter
    }
}

 

SseEmitter는 스프링부트를 사용할 경우 만료시간이 기본 30초이며, 시간이 만료되면 브라우저에서 자동으로 서버에 재연결 요청을 합니다. (생성자를 통해 만료시간을 설정할 수 있습니다.) SseEmitter를 생성할 떄는 비동기 요청이 완료되거나 타임아웃 발생 시 실행 콜백을 등록할 수 있습니다. 위 코드에서는 이벤트 발행을 비동기로 처리하기 위해 tasExecutor를 생성하여 별도 스레드에서 처리했습니다.

에러 핸들링

문제가 발생한 경우 오류 이벤트를 생성합니다. EventSource 객체에 onerror 콜백을 등록하여 에러에 대한 대응을 할 수 있습니다.

  eventSource.onerror = error => {
    console.error('EventSource failed:', error);
    eventSource.close();
  }

이벤트 스트림 형식

이벤트 스트림은 텍스트 데이터의 단순한 스트림으로 UTF-*을 사용하여 인코딩 해야합니다. 이벤트 스트림 내부 메시지는 두 개의 줄바꿈(\n\n)문자로 구분됩니다. 행 선두에 있는 콜론은 주석으로 나타내며 무시됩니다. 각 메시지는 필드를 나열한 하나 이상의 텍스트 행으로 구성됩니다. 각 필드는 `필드명:텍스트데이터`로 나타 냅니다.

  • event
    • 이벤트 유형을 식별하는 문자열이며, 이를 지정한 경우 이벤트가 브라우저에서 지정된 이벤트림으에 대한 리스너로 전달됩니다. addEventListner()에 명명된 이벤트를 수신하는데 사용합니다. 메시지에 이벤트 이름이 지정되지 않은 경우 `onmessage` 핸들러가 호출됩니다.
  • data
    • 메시지의 데이터 필드입니다. EventSource가`data:`로 시작하는  연속된 여러 줄을 수신하는 경우 이를 연결하고 사이에 개행 문자를 삽입되고, 후행 개행은 제거됩니다.
  • id
    • EventSource 객체의 마지막 이벤트 ID 값을 설정할 이벤트 ID입니다.
  • retry
    • 재접속 시간입니다. 서버 연결이 끊어지면 브라우저는 지정된 시간 동안 기다렸다가 다시 연결을 시도합니다. 재연결 시간을 밀리초 단위로 지정하는 정수여야 합니다. 정수가 아닌 값을 지정하면 이 필드는 무시됩니다.

참고

https://developer.mozilla.org/ko/docs/Web/API/Server-sent_events

https://code-lab1.tistory.com/300

https://jsonobject.tistory.com/558

https://tecoble.techcourse.co.kr/post/2022-10-11-server-sent-events/

728x90
반응형
Comments