SseEmitter (Spring)

Spring Framework์—์„œ SseEmitter๋Š” ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ Server-Sent Events(SSE)๋ฅผ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ๋ฅผ ์ „์†กํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋Š” ์„œ๋ฒ„ ์ „์†ก ์ด๋ฒคํŠธ(Server-Sent Events, SSE)๋ฅผ ๋ณด๋‚ด๊ธฐ ์œ„ํ•œ ResponseBodyEmitter์˜ ํŠน์ˆ˜ ๋ฒ„์ „์ด๋‹ค.

์ฝ”๋“œ ์š”์•ฝ

์‘๋‹ต ํ™•์žฅ:

  • extendResponse(ServerHttpResponse outputMessage): ResponseBodyEmitter์—์„œ ์˜ค๋ฒ„๋ผ์ด๋“œํ•œ ๋ฉ”์„œ๋“œ๋กœ SSE์— ๋งž๊ฒŒ ์„œ๋ฒ„ ์‘๋‹ต์„ ์ปค์Šคํ„ฐ๋งˆ์ด์ฆˆํ•ฉ๋‹ˆ๋‹ค. Content-Type์„ text/event-stream์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค(๋งŒ์•ฝ ์•„์ง ์„ค์ •๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด).

์ด๋ฒคํŠธ ๋ฐœ์†ก:

  • send(Object object): ๊ฐ์ฒด๋ฅผ ๋‹จ์ผ SSE "data" ๋ผ์ธ์œผ๋กœ ์ „์†กํ•ฉ๋‹ˆ๋‹ค.

  • send(Object object, @Nullable MediaType mediaType): ์ง€์ •๋œ MediaType์„ ๊ฐ€์ง„ ๊ฐ์ฒด๋ฅผ ์ „์†กํ•˜์—ฌ ๋ฉ”์‹œ์ง€ ์ปจ๋ฒ„ํ„ฐ ์„ ํƒ ์‹œ ์ปจํ…์ธ  ํ˜‘์ƒ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

  • send(SseEventBuilder builder): id, ์ด๋ฒคํŠธ ์ด๋ฆ„, ์ฝ”๋ฉ˜ํŠธ ๋ฐ/๋˜๋Š” ์žฌ์—ฐ๊ฒฐ ์‹œ๊ฐ„๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๋Š” ๋ณต์žกํ•œ SSE ์ด๋ฒคํŠธ๋ฅผ ์ „์†กํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒคํŠธ ๋นŒ๋”ฉ:

  • event(): SseEventBuilderImpl ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ์ •์  ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. SseEventBuilderImpl์€ ๋‚ด๋ถ€ ํด๋ž˜์Šค๋กœ SseEventBuilder ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

SseEventBuilder:

  • SseEmitter ํด๋ž˜์Šค ๋‚ด๋ถ€์˜ ์ธํ„ฐํŽ˜์ด์Šค๋กœ, SSE ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์˜ˆ๋ฅผ ๋“ค์–ด id, name, reconnectTime, comment, data ๋“ฑ์˜ ๋ฉ”์„œ๋“œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

SseEventBuilderImpl:

  • SseEventBuilder ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋‚ด๋ถ€ private static ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. SSE ํ˜•์‹์˜ ๋ฌธ์ž์—ด์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด StringBuilder๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์ „์†กํ•  ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ด๋Š” Set<DataWithMediaType>๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

DataWithMediaType:

  • ์ „์†ก๋  ๊ฐ์ฒด์™€ ๊ทธ MediaType์„ ์—ฐ๊ด€์‹œํ‚ค๋Š” ๊ฐ„๋‹จํ•œ ์ปจํ…Œ์ด๋„ˆ ํด๋ž˜์Šค๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ SseEventBuilderImpl์— ์˜ํ•ด ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

send(SseEventBuilder builder) ๋ฉ”์„œ๋“œ์—์„œ synchronized๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ์Šค๋ ˆ๋“œ ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ ์ „์†ก๋˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์ „์†ก ์ด๋ฒคํŠธ๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ์„œ๋ฒ„๊ฐ€ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋ฅผ ํด๋ผ์ด์–ธํŠธ ๋ธŒ๋ผ์šฐ์ €๋กœ ํ‘ธ์‹œํ•ด์•ผ ํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜, ์˜ˆ๋ฅผ ๋“ค์–ด ๋Œ€์‹œ๋ณด๋“œ ๋˜๋Š” ์‹ค์‹œ๊ฐ„์œผ๋กœ ์—…๋ฐ์ดํŠธ๋˜๋Š” ํ”ผ๋“œ์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ์‹œ

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Controller
@RequestMapping("/sse")
public class SseController {

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter streamSseMvc() {
        SseEmitter emitter = new SseEmitter();
        // ์ƒˆ๋กœ์šด ์Šค๋ ˆ๋“œ์—์„œ ๋น„๋™๊ธฐ๋กœ ์ฒ˜๋ฆฌ
        Executors.newSingleThreadExecutor().execute(() -> {
            try {
                for (int i = 0; i < 10; i++) {
                    // ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๊ธฐ ์ „์— ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•˜๊ฑฐ๋‚˜ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค.
                    Thread.sleep(1000);
                    emitter.send("/sse/stream - " + LocalTime.now().toString());
                }
                emitter.complete();
            } catch (IOException | InterruptedException e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }
}

Last updated

Was this helpful?