알람 서비스와 SSE

sally·2023년 3월 15일
1

알림 서비스

목록 보기
2/3

학습 하며 정리한 내용들로, 부족한 점은 양해 부탁드립니다.
알려주시면 감사하겠습니다. 🍀

앞서 LongPolling 방식으로 구현 후,
다른 URL 통한 SSE 방식의 알람 서비스 방식을 추가적으로 구현 해보며,
주요 학습 했던 것들과 느낀 차이점들을 정리해보고자 합니다.

LongPolling 🆚 SSE

기본적 차이는

  • LongPolling 은 클라이언트가 상태 체크위한 요청으로 서버 측에서 일정시간 알람 목록을 확인하면서 이벤트 발생시 1번의 응답으로 마무리 될 수 있습니다.
    • 알람 빈도가 올라가면, 알람의 실시간 전송이 중요할 수록 Polling 과 유사하게 됩니다. (빈번한 커넥션 및 데이터 조회)
  • SSE는 클라이언트가 발생한 이벤트를 받고자 구독 요청을 하며, 커넥션이 일정시간 유지되어, 서버측에서 알람 이벤트 발생시 여러 차레 응답 하는 형태 입니다.

당시 구현할 때는 큰 차이 보단 좀 불편한 느낌이었는데
Pub/Sub 으로 가면서 .. 주요하게 느낀 점이 있었습니다.
Polling 🆚 Push ?

나의 구현은 ...SSE 통한 여러 차례 응답 처리를 하지 않았습니다. 🙃
응답 후 응답을 위한 정보를 바로 삭제 해서 다음 응답을 할 수가 없게 했어요.

  • sseAlarmLocalInMemory.delete(recipientId);
  • sseAlarmLocalInMemory 가 맡아야 할 역할이 많은 거 같았고, 추가 자료구조 등 필요해 보였는데, 빠르게 다음 Redis Pub/Sub 으로 넘어가자 생각 했습니다
	sseAlarmLocalInMemory.get(recipientId)
			.ifPresentOrElse(session -> {
				session.send(Alarm.from(alarmEntity), SseSession.Error.SEND);
				sseAlarmLocalInMemory.delete(recipientId);
			}, () -> log.info("The alarm is missed. [to: {}]", recipientId));

고민했던 점들

  • sseAlarmLocalInMemory의 Map 기반 자료구조
    • 만일, 수신자에 대한 여러 유형의 알람들이 추가 된다면?
      • 빈번한 변경
  • 수신자에 대한 Queue 형식의 자료구조로 담아 처리하더라도, 동일한 사용자가 댓글을 달고 좋아요와 구독 신청을 한 번에 하면, 동일 수신자에게 동일 발신자의 다른 종류의 알람이 3번 가는게 좋을 것인지 고려해보면, 수정사항들이 많아졌습니다.
    • 클라이언트는 일정 간격으로만 구독 요청을 하게 하고, 한 번만 전송하자로 ... 단순화 해보니, 응답 후 삭제처리가 진행 됐습니다. ㅎㅎ
  • 여기서 고민 했던 점들은 다음 구현한 Redis Pub/Sub 으로 가면서 sseAlarmLocalInMemory에 많은 역할들이 있었던 점들이 보였고, 분리될 수 있는 점들을 볼 수 있었습니다. GOOD 💚

개인적으로 차이

  • SSE 는 SSE 정보를 네트워크 통한 캐시 저장소 활용시 분산 서버에서의 이용이 가능하다
  • 분리 될 수록 외부 요청처리로 트랜잭션 분리를 위해 PostService 에서의 포스트 댓글 등록 로직과 AlarmService 호출 처리를 별개로 분리 위한 별도의 layer 추가 등 리팩토링으로 성능 개선이 가능할 거 같다

💭 IF 분산서버 예상한다면 ?

  • 클라이언트에서 EventSource 통해 요청
  • 요청(1번 수신자)을 받은 서버가 응답 할 정보를 네트워크 통한 캐시 레이어의 InMemory 저장소에 저장
  • 다른 클라이언트가 (1번 수신자가 작성한 게시글에) 댓글을 등록하면(8번 사용자) 해당 요청 처리를 다른 서버가 하게 한다면
  • 그림에서 CACHE라 명한 공통 저장소를 통해 댓글 등록시 해당 응답 정보로 알람 전송 처리도 일어나게 할 거 같아요

Class SseEmitter

사실 적용까진 하지 않았지만, 정리 해 봅니다.

public SseEmitter()
public SseEmitter(Long timeout)
  • timeout 설정 안하면 , the underlying server 에 의해 결정 된다고 하는데요.. ?

  • 톰캣 설정을 조금 봐보면

    • docs.spring.io
    • server.tomcat.connection-timeout
      • 연결 수락 후 커넥터 대기 시간
    • server.tomcat.keep-alive-timeout
      • 미 설정시 Connection Timeout으로 세팅
      • 커넥션 종료 전 다른 HTTP 요청 대기시간
      • -1 : no timeout
  • javadoc-api

public void send(Object object) throws IOException


public void send(Object object, @Nullable MediaType mediaType)
		  throws IOException
// static import of SseEmitter.*
 SseEmitter emitter = new SseEmitter();
 emitter.send(event().data(myObject, MediaType.APPLICATION_JSON));



public void send(SseEmitter.SseEventBuilder builder)
          throws IOException
// static import of SseEmitter
SseEmitter emitter = new SseEmitter();
emitter.send(event().name("update").id("1").data(myObject));


public static SseEmitter.SseEventBuilder event()
  • MediaType 힌트로 HttpMessageConverter 선택

    스프링 부트는 요청 타입 설정시 자동으로 응답 타입 결정된다.

    • 별도의 응답 타입 지정 없이 아래와 같은 결과의 응답 헤더 확인 할 수 있었다.

  • SseEventBuilder 는 이밴트를 포맷하기 위한 이용

  • 예외 발생은 ResponseBodyEmitter 의 send() 기준

    • 스프링 MVC 가 예외 처리하는 메커니즘 통해 전달할 앱 서버로 dispatch
    • 이는 별도 처리 위한 completeWithError(Throwable) 호출하거나 서블릿 컨테이너 통한 처리등 필요없다.
  • 완료 or 타임아웃시

    • 콜백 패턴 위임받는 내부 클래스 이용한(ResponseBodyEmitter) 리팩토링
        sseSession.checkEmitter(
            () -> sseAlarmLocalInMemory.delete(sseSession.recipientId()));


그외 배포시 여러 에러 사항들

저는 배포하지 않아서 정리만 해봅니다.
자세한 내용은 아래 블로그를 참고 하면 좋을 거 같습니다.

Spring에서 Server-Sent-Events 구현하기

요약

( 자세한 내용은 해당 블로그를 참고해 주세요.)

  • Client
    • 이벤트 구독 위한 요청 = 알람 전송할 정보
    • Request
      • Request-line : ~ HTTP 1.1 ( for 지속적 연결 )
      • Accept: text/event-stream
      • Cache-Control: no-cache
    • EventSource 인터페이스 이용한 SSE 연결 요청
  • Server
    • client 로 부터 받은 브라우저 정보를 연결된 시간 동안 응답
    • Response
      • Response-line : HTTP/1.1 200
      • Content-Type: text/event-stream;charset=UTF-8
        • 데이터 전송시 데이터는 UTF-8로 인코딩된 텍스트 데이터만 가능
          • 바이너리 데이터는 전송 불가능
      • Transfer-Encoding: chunked
        • 스트리밍 때문에 본문의 크기를 알 수 없기 때문에 필요한 설정
    • spring framework 4.2부터 SSE 통신을 지원하는 SseEmitter API를 제공
      • SSE 요청에 대한 응답 기능
    • 503 Service Unavailable 에러
      • Emitter 생성 후 만료시간 까지 보낸 데이터 없으면, 클라이언트에서 재연결 요청시 에러 발생

      • 에러 방지 위해 처음 연결시 더미 데이터를 응답

        // SseAlarmService - connect()
        sseSession.send(ALARM_CONNECTION_MESSAGE, SseSession.Error.CONNECTION);
    • 헤더 통한 토큰 전달
      • EventSource 인터페이스는 기본적으로 헤더 전달을 지원하지 않는다
    • JPA 사용시 Connection 고갈 문제
      • SSE 통신을 하는 동안은 HTTP Connection이 계속 열려 있어,
        • SSE 연결 응답 API 에서 JPA 사용시 open-in-view 속성이 true 이면
          • HTTP Connection이 열려있는 동안 DB Connection도 지속
          • open-in-view 설정 반드시 false로 설정
    • Nginx 사용시 주의할 점
      • Nginx는 기본적으로 Upstream으로 요청을 보낼때 HTTP/1.0 버전 사용
        • 위에서 Response-line : HTTP/1.1 200 필요

        • Nginx에서 백엔드 WAS로 요청시 Connection: close 헤더 사용으로 연결 지속 X

          proxy_set_header Connection '';
          proxy_http_version 1.1;
      • Nginx의 proxy buffering 기능도 조심
        • Request 헤더 Transfer-Encoding: chunked 사용
        • Nginx는 서버의 응답을 버퍼에 저장해두었다가 버퍼가 차거나 서버가 응답 데이터를 모두 보내면 클라이언트로 전송하게 됩니다.
          • SSE 통신 시 원하는대로 동작하지 않거나 실시간성이 떨어지게 된다
          • SSE 응답에 대해서는 proxy buffering 설정을 비활성화 ?
            • 모든 API 응답에 대해서도 버퍼링을 하지 않기 때문에 비효율적
            • nginx의 X-accel 기능을 활용
            • SSE 응답을 반환하는 API의 헤더에 X-Accel-Buffering: no
profile
sally의 법칙을 따르는 bug Duck

0개의 댓글