본문 바로가기
Spring/SpringBoot

[SpringBoot] SSE (Server-Sent Events) 사용하여 실시간 통신하기

by J4J 2024. 5. 7.
300x250
반응형

안녕하세요. J4J입니다.

 

이번 포스팅은 sse (server-sent events) 사용하여 실시간 통신하는 방법에 대해 적어보는 시간을 가져보려고 합니다.

 

 

 

관련 글

 

[React] SSE (Server-Sent Events) 사용하여 실시간 통신하기

 

 

 

 

SSE (Server-Sent Events) 란?

 

sse는 서버로부터 클라이언트에 실시간으로 데이터를 전달할 수 있는 기술 중 하나입니다.

 

일반적으로 sse와 많이 비교대는 것은 socket이 존재합니다.

 

이해를 먼저 돕기 위해 socket과 sse의 특징들에 대해서 정리하면 다음과 같습니다.

 

  Socket SSE (Server-Sent Events)
프로토콜 socket http
데이터 전달 방향 client와 server 간 양 방향 통신 가능 server에서 client로 단 방향으로만 통신 가능
최대 접속 수 서버 설정에 따라 다름 http2 기준 브라우저 당 최대 100개
실시간 통신 여부 실시간 통신 실시간 통신
자동 재 연결 자동 재 연결 지원하지 않음 자동 재 연결 지원
리소스 사용 sse보다 더 많은 메모리 및 cpu 필요 socket보다 더 적은 메모리 및 cpu 필요

 

 

 

최근에 socket 같은 경우는 web socket 기반으로 많이 사용되기 때문에 web socket에 대해서 비교하면 위의 표와는 정보가 다를 수 있습니다.

 

web socket도 http 기반으로 동작되고, 자동 재 연결도 지원되는 등의 특징들이 존재하기 때문에 참고하시면 될 것 같습니다.

 

 

반응형

 

 

추가로 socket과 sse의 구조도도 그려보면 다음과 같습니다.

 

socket 구조도

 

sse 구조도

 

 

 

sse와 socket에 대한 특징 및 구조도를 확인해보면서 어떤 유형의 기술인지 어느 정도 이해가 되셨을 거라고 생각합니다.

 

client와 server간 실시간 통신을 해야 된다고 할 때 일반적으로 많이 생각하시는 것은 socket이 존재합니다.

 

하지만 socket 말고도 sse 기술을 통해 실시간 통신을 동일하게 구현할 수 있는 것을 인지할 필요가 있습니다.

 

 

 

 

그러면 이해를 더 돕기 위해 어느 상황에 socket과 sse를 각각 사용하는지 예시를 들어보겠습니다.

 

socket 같은 경우는 동일한 connection을 이용하여 client에서도 server에 데이터가 전달되고 반대로 server에서도 client에 전달되어야 합니다.

 

그래서 많이들 알고 계시는 것처럼 실시간 채팅 / 실시간 화상 통화 등 client와 server 모두 데이터를 실시간으로 전달해야 되는 상황에서 많이 사용됩니다.

 

 

 

sse 같은 경우는 client 측에서 server에 구독 과정을 통해 connection을 맺고 server에서만 client에 실시간으로 데이터를 전달합니다.

 

그래서 server 내부 비즈니스 로직을 통해서 client에 전달할 필요가 있는 알림 기능과 같은 것들이 많이 사용됩니다.

 

결과적으로 위에 정리해둔 것처럼 sse가 socket 보다는 사용되는 리소스가 적은 편이며 또한 구현 방식도 더 간단한 편에 속하기에 client에서 server에 실시간으로 데이터를 전달할 필요가 없다면 socket보다는 sse를 선택하는 것을 권장드립니다.

 

 

 

 

SSE (Server-Sent Events) 사용 환경 설정

 

이번에는 spring을 이용하여 sse 사용 환경 설정하는 방법에 대해 적어보겠습니다.

 

sse 사용 환경 설정하는 방법은 다음과 같습니다.

 

 

 

[ 1. request dto 클래스 구성 ]

 

package com.jforj.sse.dto;

public record SseSendRequest(
        String eventName,
        Object data
) {
}

 

 

 

[ 2. controller 클래스 구성 ]

 

package com.jforj.sse.controller;

import com.jforj.sse.dto.SseSendRequest;
import com.jforj.sse.service.SseService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@RestController
@RequiredArgsConstructor
public class SseController {
    private final SseService sseService;

    @GetMapping("/subscribe/{id}")
    public SseEmitter subscribe(@PathVariable String id) {
        return sseService.subscribe(id);
    }

    @PostMapping("/send/{id}")
    public void sendAlarm(@PathVariable String id, @RequestBody SseSendRequest sseSendRequest) {
        sseService.sendToClient(id, sseSendRequest.eventName(), sseSendRequest.data());
    }
}

 

 

 

[ 3. service 클래스 구성 ]

 

package com.jforj.sse.service;

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

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class SseService {
    private final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>(); // id별 emitter 보관

    /**
     * sse를 통한 구독 기능 정의
     */
    public SseEmitter subscribe(String id) {
        long timeout = 1000L * 60 * 60; // sse emitter 연결 시간, 1시간

        // sseEmitter 저장
        SseEmitter sseEmitter = new SseEmitter(timeout);
        sseEmitterMap.put(id, sseEmitter);

        // sseEmitter complete 처리
        sseEmitter.onCompletion(() -> sseEmitterMap.remove(id));
        // sseEmitter timeout 발생
        sseEmitter.onTimeout(sseEmitter::complete);
        // sseEmitter error 발생
        sseEmitter.onError(throwable -> sseEmitter.complete());

        // connect event로 message 발생
        sendToClient(id, "connect", "sse connect...");

        return sseEmitter;
    }

    /**
     * sse를 통해 client에 데이터를 전달
     * id에 해당되는 sse emitter에 event name의 이벤트로 data 전달
     */
    public void sendToClient(String id, String eventName, Object data) {
        SseEmitter sseEmitter = sseEmitterMap.get(id);
        try {
            sseEmitter.send(
                    SseEmitter
                            .event()
                            .id(id)
                            .name(eventName)
                            .data(data)
            );
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

 

 

 

 

SSE (Server-Sent Events) 사용 환경 설정 테스트

 

위와 같이 코드를 작성한 뒤 spring 서버를 구동시켜 보겠습니다.

 

먼저 다음과 같이 subscribe API를 서버에 전달하여 id가 jforj인 sse emitter를 구독해 보겠습니다.

 

sse emitter 구독

 

 

 

결과를 확인해 보면 event name이 connect의 형태로 메시지가 전달된 것을 볼 수 있습니다.

 

해당 event name은 front에서 코드를 작성할 때 event listener name으로 활용됩니다.

 

 

 

 

다음으로 send API를 서버에 전달하여 구독된 sse emitter를 보유하고 있는 client에 메시지를 전달해 보겠습니다.

 

그러면 다음과 같이 구독을 했던 API 쪽에 event name에 맞게 message가 전달된 것을 볼 수 있습니다.

 

alarm send

 

alarm send response

 

 

 

 

마지막으로 데이터를 변경하여 send API를 서버에 전달해 보겠습니다.

 

그러면 위와 동일하게 event name에 맞게 메시지가 전달되는 것을 볼 수 있습니다.

 

message send

 

message send response

 

 

 

 

 

 

 

 

이상으로 sse (server-sent events) 사용하여 실시간 통신하는 방법에 대해 간단하게 알아보는 시간이었습니다.

 

읽어주셔서 감사합니다.

 

 

 

728x90
반응형

댓글