우와한 개발자

[Spring] SSE(Server-Sent Events)란? EventSource, SseEmitter 구현

by 우와한개발자

1. Server-SentEvent란?

  • 서버에서 클라이언트로 단방향 실시간 데이터를 스트리밍하는 기술
  • WebSocket과 달리 HTTP 프로토콜을 그대로 사용
  • 서버가 클라이언트에게 일방적으로 데이터를 푸시

 

1) SSE vs WebSocket 비교

구분 SSE WebSocket
통신 방향 단방향 (서버 → 클라이언트) 양방향
프로토콜 HTTP ws:// (별도 프로토콜)
자동 재연결 브라우저가 자동 처리 직접 구현 필요
구현 난이도 쉬움 복잡함
사용 사례 알림, 뉴스피드, 진행률 채팅, 게임, 실시간 협업

 

2. EventSource 객체

  • 클라이언트(브라우저)에서 SSE 연결을 수립하고 서버로부터 이벤트를 수신하는 브라우저 내장 API

 

1) EventSource 속성/이벤트

속성/이벤트 설명
readyState 서버와의 접속 상태를 나타냄. (상수로 확인 가능).
- CONNECTING : 서버에 접속 요청 중 (0)
- OPEN : 서버에 접속됨 (1)
- CLOSED : 서버에 접속되지 않음 (2)
open 서버와 접속이 되면 발생
message 서버로부터 응답이 도착하면 발생
error 오류 발생 시 또는 연결이 끊어졌을 때 발생
close() EventSource 연결을 직접 종료하는 메서드
const es = new EventSource("/sse/connect");

console.log(es.readyState); // 0 : CONNECTING

es.onopen = () => {
    console.log(es.readyState); // 1 : OPEN
};

es.onmessage = (event) => {
    console.log(event.data);
};

es.onerror = () => {
    console.log(es.readyState); // 2 : CLOSED
};

 

2) EventSource 객체 생성

const eventSource = new EventSource("/sse/connect");

 

3) 데이터 수신

// 기본 메시지 수신 (이벤트명 없는 경우)
eventSource.onmessage = (event) => {
    console.log("받은 데이터 : " + event.data);
};

// 특정 이벤트명으로 수신
eventSource.addEventListener("notification", (event) => {
    console.log("알림 : " + event.data);
});

// 연결 수립 시
eventSource.onopen = () => {
    console.log("SSE 연결 성공");
};

// 오류 발생 시
eventSource.onerror = (error) => {
    console.error("SSE 오류 : ", error);
    eventSource.close();  // 연결 종료
};

 

4) 서버의 응답 데이터 형식

  • SSE 응답은 text/event-stream Content-Type으로
  • 각 필드는 필드명: 값 형식으로 구성
data: 안녕하세요\\n\\n

event: notification\\n
data: 새 알림이 있습니다\\n\\n

id: 1\\n
event: update\\n
data: {"name": "홍길동"}\\n
retry: 3000\\n\\n

 

(1) 사용할 수 있는 필드명

  • 각 이벤트는 빈 줄(\n\n)로 구분
  • data만 필수이고 나머지는 선택.
필드명 설명
data 전송할 실제 데이터 (필수)
event 이벤트 이름 (생략 시 onmessage로 수신)
id 이벤트 ID (재연결 시 Last-Event-ID 헤더로 전달)
retry 재연결 대기 시간 (밀리초)

 

3. SSE 구현 - Spring Boot (Maven)

1) pom.xml

  • SSE는 별도 의존성 없이 spring-boot-starter-web만 있으면 사용 가능
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

 

2) Controller

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

    // 연결된 emitter 목록 관리
    private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();

    // 클라이언트 연결
    @GetMapping("/connect")
    public SseEmitter connect() {
        SseEmitter emitter = new SseEmitter(60 * 1000L);  // 60초 타임아웃

        emitters.add(emitter);

        // 연결 종료/타임아웃/오류 시 목록에서 제거
        emitter.onCompletion(() -> emitters.remove(emitter));
        emitter.onTimeout(() -> emitters.remove(emitter));
        emitter.onError(e -> emitters.remove(emitter));

        return emitter;
    }

    // 모든 클라이언트에게 이벤트 전송
    @PostMapping("/send")
    public ResponseEntity<Void> send(@RequestParam String message) {
        for (SseEmitter emitter : emitters) {
            try {
                emitter.send(SseEmitter.event()
                        .name("notification")    // event 필드
                        .data(message));         // data 필드
            } catch (IOException e) {
                emitters.remove(emitter);
            }
        }
        return ResponseEntity.ok().build();
    }
}

 

4. SSE 구현 - Spring Legacy

1) pom.xml

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.3.30</version>
</dependency>

 

2) Controller

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

    private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();

    @GetMapping("/connect")
    @ResponseBody
    public SseEmitter connect() {
        SseEmitter emitter = new SseEmitter(60 * 1000L);

        emitters.add(emitter);
        emitter.onCompletion(() -> emitters.remove(emitter));
        emitter.onTimeout(() -> emitters.remove(emitter));

        return emitter;
    }

    @PostMapping("/send")
    @ResponseBody
    public ResponseEntity<Void> send(@RequestParam String message) {
        for (SseEmitter emitter : emitters) {
            try {
                emitter.send(SseEmitter.event()
                        .name("notification")
                        .data(message));
            } catch (IOException e) {
                emitters.remove(emitter);
            }
        }
        return ResponseEntity.ok().build();
    }
}

 

5. Spring Boot vs Spring Legacy 비교

구분 Spring Boot Spring Legacy
의존성 spring-boot-starter-web (기본 포함) spring-webmvc 별도 추가
설정 방식 자동 설정 수동 설정 필요
SseEmitter 동일하게 사용 동일하게 사용
배포 방식 JAR 또는 WAR WAR (외부 톰캣)

 

블로그의 정보

우와한개발자 님의 블로그

우와한개발자

활동하기