티스토리 뷰

Spring

Spring boot SSE 예제 코드(심플)

현오쓰 2024. 2. 15. 15:54

DB에 실시간으로 쌓이는 데이터를 실시간으로 화면에 보여주고 싶었다.

SSE를 모를 땐  WebSocket으로 구현했었다.

그런데, 클라이언트 쪽에서 데이터를 계속 받으려면 서버쪽에 다시 요청을 해야하는데..

어쩌다 보니 폴링으로 해결했다. (SSE를 알았더라면 변경했을 것 이다..😮)

 

SSE(Server-Sent Events)는 서버에서 클라이언트로 단방향 통신을 할 수있다.

채팅프로그램이 아닌  실시간으로 데이터 받기만 한다면 웹소켓보단 SSE로 구현하는게 좋다.

 

*SSE는 UTF-8 데이터만 보낼 수 있고, 바이너리 데이터는 지원하지 않는 것만 주의하자.

 

웹소켓 vs SSE

 

 

개발 환경

- java 17

- spring boot 3.2.1

- gradle

Controller 

package com.example.study.controller;

import java.io.IOException;
import java.io.Writer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.example.study.service.SseService;

@RestController
public class MainController {

    @Autowired
    private SseService sseService;

    // UTF-8 데이터만 보낼 수 있음, 바이너리 데이터 지원 x 
    @GetMapping(path = "/emitter", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter subscribe(){
        //SseEmitter는 서버에서 클라이언트로 이벤트를 전달할 수 있습니다.
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE);
        sseService.addEmitter(emitter);
        sseService.sendEvents();
        return emitter;
    }
}

 

Service

package com.example.study.service;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Service
public class SseService {
    /*
     * 주로 순회가 일어나는 용도로 사용할 때는 안전한 스레드 처리를 위해 CopyOnWriteArrayList를 사용
     */
    private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();

    public void addEmitter(SseEmitter emitter) {
        emitters.add(emitter);
        emitter.onCompletion(() -> emitters.remove(emitter));
        emitter.onTimeout(() -> emitters.remove(emitter));
    }

    @Scheduled(fixedRate = 1000)
    public void sendEvents() {
        for (SseEmitter emitter : emitters) {
            try {
                emitter.send("Hello, World!");
            } catch (IOException e) {
                emitter.complete();
                emitters.remove(emitter);
            }
        }
    }
}

 

Main

package com.example.study;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

//@EnableScheduling 추가
@SpringBootApplication
@EnableScheduling
public class StudyApplication {

	public static void main(String[] args) {
		SpringApplication.run(StudyApplication.class, args);
	}

}

 

resources -> static -> index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Spring SSE Demo</title>
</head>
<body>
    <h1>Server-Sent Events (SSE) with Spring</h1>
    <div id="events"></div>

    <script>
    	
        const eventSource = new EventSource("/emitter");//controller 경로

        eventSource.onmessage = (event) => { //데이터를 받아옴
            const div = document.createElement("div");
            div.textContent = `Event received: ${event.data}`;
            document.getElementById("events").appendChild(div);
        };
        eventSource.onerror = (error) => {
            console.error("Error occurred:", error);
            eventSource.close();
        };
    </script>
</body>
</html>

 

 

마치며,

클라이언트 쪽에서 폴링 or 롱폴링 해도 상관없다.

백엔드에서 최대한 로직을 구현하고 클라이언트쪽에 넘겨주는걸 선호하는 편이다.😅