본문 바로가기
[개발] 프레임워크/Spring

Spring에서 SSE(Server-Sent Event) 구현하기

by Devsong26 2024. 9. 8.

Server-Sent Event(이하 SSE)는 서버에서 클라이언트로 실시간 데이터를 푸시하는 단방향 통신 방식입니다. 클라이언트가 서버와의 연결을 설정하면, 서버는 지속적으로 데이터를 클라이언트로 전송할 수 있습니다. SSE는 HTML5 표준으로, 브라우저에서 이벤트 스트림을 수신하는 기능을 제공합니다.

 

SSE의 특징

  1. 단방향 통신: 클라이언트가 서버에 연결을 설정한 후, 서버는 실시간으로 클라이언트에게 데이터를 보낼 수 있지만, 클라이언트는 이 연결을 통해 서버로 데이터를 전송할 수 없습니다. 클라이언트에서 서버로는 일반적인 HTTP 요청을 통해 데이터를 전송해야 합니다.
  2. 텍스트 데이터 전송: SSE는 서버에서 클라이언트로 텍스트 데이터를 전송합니다. JSON 같은 형식으로 데이터를 보내는 것이 일반적입니다.
  3. 브라우저 지원: SSE는 대부분의 최신 웹 브라우저에서 지원됩니다. JavaScript의 EventSource 객체를 사용하여 쉽게 구현할 수 있습니다.
  4. 자동 재연결: 연결이 끊어진 경우, 브라우저가 자동으로 재연결을 시도하는 기능이 내장되어 있습니다. 이를 통해 일시적인 네트워크 문제에도 연결이 지속될 수 있습니다.
  5. 단일 HTTP 연결: WebSocket처럼 별도의 프로토콜을 사용하지 않고, 단일 HTTP 연결을 유지하면서 서버에서 클라이언트로 데이터를 지속적으로 보낼 수 있습니다.

 

SSE의 사용 사례

  • 실시간 알림 시스템: 서버에서 발생하는 중요한 이벤트(알림, 채팅 메시지 등)를 클라이언트에게 실시간으로 전송.
  • 주식 가격 또는 실시간 데이터 피드: 실시간으로 변화하는 데이터를 클라이언트에게 스트리밍.
  • 대시보드 업데이트: 실시간으로 서버 상태나 통계 데이터를 클라이언트에게 전달.

 

SSE의 장점

  • 단순성: HTTP를 기반으로 하므로 기존 인프라에서 쉽게 사용할 수 있습니다.
  • 자동 재연결: 클라이언트가 끊어진 경우 자동으로 재연결을 시도합니다.
  • 다수의 브라우저 지원: 대부분의 현대적인 웹 브라우저에서 지원되며, 설정이 간단합니다.

 

SSE의 단점

  • 단방향: 클라이언트에서 서버로의 데이터 전송은 지원되지 않으므로, 양방향 통신이 필요한 경우 WebSocket이 더 적합합니다.

 


 

실습을 통해 SSE에 대해서 알아보겠습니다.

 

동작원리

 

  1. 클라이언트가 서버로 EventSource(SseEmitter)를 요청합니다.
  2. 서버는 클라이언트 식별자를 이용하여 SseEmitter를 생성합니다.
  3. 서버는 생성된 SseEmitter를 클라이언트에 응답합니다.
  4. 서버에서 이벤트가 발생합니다.
  5. 서버는 클라이언트에 이벤트 메시지를 전송합니다.

 

코드

  • 클라이언트가 서버로 EventSource(SseEmitter)를 요청합니다.
// 서버에 eventSource 요청
const eventSource = new EventSource('/notifications');

eventSource.addEventListener('notification', function(event) {
    // 이벤트 메시지를 수신하면 처리될 로직
    // event.data -> 이벤트 메시지
});

eventSource.onerror = function() {
    eventSource.close();
};

 

eventSource.addEventListener(type, listener[, options]);

type: 서버가 보내는 이벤트의 유형

listener: 수신된 이벤트를 처리하는 리스너

 

  • 버는 클라이언트 식별자를 이용하여 SseEmitter를 생성합니다. 
  • 버는 생성된 SseEmitter를 클라이언트에 응답합니다.
public class NotificationController {

    private final SseEmitterService sseEmitterService;

    @GetMapping("/notifications")
    public SseEmitter subscribe(@AuthenticationPrincipal MemberPrincipal principal) {
        // clientId는 커스텀으로 구현해야 합니다.
        return sseEmitterService.createOrGetEmitter(principal.clientId());
    }
}

public class SseEmitterService {

    private final ConcurrentHashMap<String, SseEmitter> sseMap = new ConcurrentHashMap<>();

    public SseEmitter createOrGetEmitter(String clientId){
        if(sseMap.containsKey(clientId)){
            return sseMap.get(clientId);
        }

        final SseEmitter sseEmitter = new SseEmitter(-1L);
        sseMap.put(clientId, sseEmitter);

        sseEmitter.onCompletion(() -> sseMap.remove(clientId));
        sseEmitter.onTimeout(() -> sseMap.remove(clientId));
        sseEmitter.onError(e -> sseMap.remove(clientId));

        return sseEmitter;
    }
}

 

클라이언트의 요청이 많아질수록 서버가 매번 SseEmitter 객체를 생성하면 리소스 부담이 커집니다.

클라이언트당 생성되는 SseEmitter는 한 개로 제한합니다.

SseEmitter는 메시지를 전송하거나, 타임아웃이 발생하거나, 에러가 발생할 경우 SseMap에서 제거합니다.

 

 

  • 버에서 이벤트가 발생합니다.
  • 버는 클라이언트에 이벤트 메시지를 전송합니다.
public class EventHandler {

    private final SseEmitterService sseEmitterService;

    @TransactionalEventListener
    public void sendToClient(Event event){
        try{
            sseEmitterService.sendNotification(event.clientId(), 메시지);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
    
}


public class SseEmitterService {
	
    ...
    
    public void sendNotification(String clientId, String message) throws IOException {
        final SseEmitter emitter = sseMap.get(clientId);
        if (emitter != null) {
            emitter.send(SseEmitter.event().name("notification").data(message));

            try{
                emitter.complete();
            }catch(Exception ex){
                log.error(ex.getMessage());
                emitter.completeWithError(ex);
            }

        }
    }
}

 

SseEmitter는 complete(), completeWithError() 메서드를 통해 사용된 SseEmitter는 종료시킵니다.

클라이언트에서 선언한 이벤트 타입인 'notification'으로 서버는 메시지를 보내야 합니다.