SSE란 무엇인가?

Jimin·2022년 11월 23일
0
post-thumbnail

SSE란 무엇인가?

Server Sent Events

  • SSE는 서버의 데이터를 실시간으로, 지속적으로 Streaming 하는 기술 이다.
  • SSE는 웹 표준으로써 IE를 제외한 모든 브라우저에서 지원되며, IE역시 polyfill을 통해 지원이 가능하다.

SSE 특징

  • 브라우저는 서버가 생성 한 Stream 을 계속 받음 (Server 에서 보내는 Stream 으로 Read Only)
  • Connection 유지를 위해 HTTP protocol 을 사용, HTTP/2를 통한 multiplexing 사용 가능
  • 연결이 끊어지면 EventSource가 오류 이벤트를 발생시키고 자동으로 다시 연결을 시도 (error recovery)
  • 표준 기술로 IE 를 제외한 브라우저 대부분을 지원 (Pollyfill로 IE 사용 가능)

Web Socket과 SSE의 차이점

websocket(server push)

  • websocket은 연결을 유지하여 서버와 클라이언트 간 양방향 통신이 가능

SSE(server push)

이벤트가 [서버 -> 클라이언트] 방향으로만 흐르는 단방향 통신 채널


cf) Short Polling vs Long Polling

Short Polling

  • 클라이언트가 주기적으로 서버로 요청을 보내느 방법
  • 일정 시간마다 서버에 요청을 보내서 데이터가 갱신되었는지 확인한 다음 만약 갱신되었다면 데이터를 응답 받는 구조
  • 클라리언트와 서버 모두 구현이 단순함
  • 서버가 요청에 대한 부담이 크지 않고 요청 주기를 여유롭게 잡아도 될 정도로 실시간성이 중요하지 않다면 고려해볼만한 방법

Long Polling

  • 요청을 보내고 서버에서 변경이 일어날 때까지 대기하는 방법
  • 실시간 메시지 전달이 중요하지만 서버의 상태가 빈번하게 변하지 않는 경우에 적합함
  • 서버로부터 응답을 받고 나면 다시 연결 요청을 하기 때문에, 상태가 빈번하게 바뀐다면 연결 요청도 늘어남

버스 탑승자가 예약할 경우 버스 기사에게 알림이 발생하는 기능을 개발 중입니다.

  • 알림 대상: 버스 기사 <- 버스 탑승자

Spring을 이용한 SSE 개발

SSE Service

1. sse 구독

@Service
@AllArgsConstructor
public class NotificationService {
    private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60;
    private final EmitterRepository emitterRepository; 
    public SseEmitter subscribe(String dId, String lastEventId) {
        // 1
        String id = dId + "_" + System.currentTimeMillis();

        // 2
        SseEmitter emitter = emitterRepository.save(id, new SseEmitter(DEFAULT_TIMEOUT));

        emitter.onCompletion(() -> emitterRepository.deleteById(id));
        emitter.onTimeout(() -> emitterRepository.deleteById(id));
        emitter.onError((e) -> emitterRepository.deleteById(id));

        // 3
        // 503 에러를 방지하기 위한 더미 이벤트 전송
        String eventName = "503 에러 방지용 이벤트";
        EventDTO eventDTO = new EventDTO("EventStream Created.", dId);
        sendToClient(emitter, id, eventDTO, eventName, 1);

        // 4
        // 클라이언트가 미수신한 Event 목록이 존재할 경우 전송하여 Event 유실을 예방
        if (!lastEventId.isEmpty()) {
            Map<String, Object> events = emitterRepository.findAllEventCacheWithId(String.valueOf(dId));
            events.entrySet().stream()
                    .filter(entry -> lastEventId.compareTo(entry.getKey()) < 0)
                    .forEach(entry -> sendToClient(emitter, entry.getKey(), entry.getValue(), "미수신한 이벤트 목록", 1));
        }

        return emitter;
    }
}
  • HTTP 503 상태 코드(Service Unavailable)는 일반적으로 오리진 서버의 성능 문제를 나타낸다. 드물지만 이 상태 코드가 엣지 로케이션의 리소스 제한 때문에 CloudFront가 일시적으로 요청을 충족할 수 없음을 나타낸다.

2. 클라이언트에게 데이터 요청 준비

    private void sendToClient(SseEmitter emitter, String key, Object data, String name, int option) {

        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {
            try {
                emitter.send(SseEmitter.event()
                        .id(key)
                        .name(name)
                        .data(data, MediaType.APPLICATION_JSON)
                        .reconnectTime(0));

            } catch (Exception e) {
                if (option == 1)
                    emitterRepository.deleteById(key);
                else sseRatingRepository.deleteById(key);

                System.out.println(e.getMessage());
            }
        });
    }

3. 실제 클라이언트에게 알림 전송

    public void send(String  dId, Long rId, String startPoint, String  endPoint, LocalDate rTime ) {
        Notification notification = createNotification(rId, startPoint, endPoint, rTime);

        //관련된 SseEmitter 모두 가져오기
        Map<String, SseEmitter> sseEmitters = emitterRepository.findAllStartWithById(dId);

        sseEmitters.forEach(
                (key, emitter) -> {
                    // 데이터 캐시 저장(유실된 데이터 처리하기 위함)
                    emitterRepository.saveEventCache(key, notification);
                    // 데이터 전송
                    sendToClient(emitter, key, notification,  "사용자에 의한 예약 발생", 1);
                    log.info("[driverId: " + dId + "]: " + notification);
                }
        );
    }

Spring을 통한 테스트 과정

  • subscribe 성공 시 log에 SseEmitter 정보 출력
  • 출력값: 1_XXXX

  • 값이 넘어오긴 함

테스트 결과

버스 기사 로그인

잘 안 보일 수도 있지만...
버스 기사 id : 1 (url 안에 있음)
로그인 후 계속 대기(?) 중

사용자 예약(1)


사용자 예약(2)


사용자 예약(3)

  • 현재 로그인 중인 버스 기사(id: 1) 외에 다른 버스 기사(id: 2)에게 배정된 버스에 예약이 될 경우

  • 위에 2개의 테스트와 달리 콘솔창에 log 안 찍힘
    • 데이터 전송이 발생하지 않았기 때문!!
  • 로그인 중인 버스 기사가 운행하는 버스에 대한 예약이 발생했을 때만 데이터 전송이 발생함

직접 포스트맨으로 결과를 얻어보고 싶었지만 연결을 끊은 후에만 모든 이벤트 정보들이 출력됨 그래서 로그로 찍은 거!!
내가 원했던 건 예약 요청이 되면 바로바로 그 알림 메시지? 보여주는 거였는데... 당연히 return을 해야 값이 넘어오는데 sse 연결이 끊어지지 않는 한 값이 안 넘어오는 게 맞는건가...!?

  • 만약 연결을 끊어준다면?
  • 참고
    데이터 보낼 때
    data:{"startSeq":1,"endSeq":2,"rTime":"2022-11-24"}
    이렇게 data만 보내는 것도 가능함
로그인 후 원래 response값은 버스 기사 정보와 버스 시간표인데 현재 sse 테스트를 위해 이 부분을 생략함 추후 수정 예정임 

0개의 댓글