Framework/Spring

[Spring Cloud Gateway] Gateway와 SSE 사용시 데이터 수신

비소_ 2023. 1. 2.

요약

1. 서버를 분리하면서 SSE를 이용한 실시간 알림 수신 기능에 문제 발생

2. 요청도 정상적으로 갔고, 실제 서버에서 처리도 했지만, 응답만 못받는 채로 타임아웃/재연결 무한반복

3. Spring Cloud Gateway에서 CORS 관련 문제로 응답을 보내주지 않았음

4. application.yml 설정으로 간단히 해결


개요

기존 구현 코드

해당 프로젝트 초기에는 Front-End와 Back-End 분리 없이 하나의 서버에서 모든 과정을 수행했었다.

기능이 점점 추가되면서 각 기능별로 분리하게 됐다.

ver.3 아키텍처

위 아키텍처로 가기 전에 하나씩 분리하면서 테스트를 통해 기존 기능이 정상적으로 수행하는지 확인했는데, 그 중 실시간 알림 기능이 문제였다.

기능 중 한가지로 Server Sent Event(SSE) 방식으로 실시간 알림을 수신하는 기능이 있었는데, 다음과 같다.

//legacy code
@GetMapping(value = "/subscribe", produces = "text/event-stream")
@ResponseStatus(HttpStatus.OK)
public SseEmitter subscribe(
        @AuthenticationPrincipal UserDetails userDetails,
        @RequestHeader(value = "Last-Event-ID", required = false, defaultValue = "") String lastEventId
) {
    EmitterAdaptor emitterAdaptor = EmitterAdaptor.builder()
            .userEmail(userEmail)
            .lastEventId(lastEventId)
            .build();

    return notyService.subscribe(emitterAdaptor);
}

하나의 서버일 때는 정상적으로 작동했지만, 서버를 분리하고 나서는 Event Stream을 수신받지 못했다.

당시, Event Stream을 수신받는 JS 코드이다.

//legacy code
const eventSource = new EventSource("/api/noty/subscribe");

eventSource.addEventListener("message", function (event) {
    try {
        const data = JSON.parse(event.data) //데이터 JSON 변환

        //알림 갯수 증가
        const $noty = $('#noty-count');
        const current = parseInt($noty.text());
        $noty.text(current + 1);

        addNoty(data); //알림창에 알림 추가

        $("#noty").dialog("open"); //알림창 open
    } catch (e) {
        console.error(e)
    }
});

요청은 정상적으로 전달됐고, 실제 API 서버에서도 SseEmitter를 정상적으로 반환해줬다.

하지만, 클라이언트가 응답을 받지 못한채 연결 대기상태를 유지하면서 timeout과 재연결 시도를 무한히 반복하고 있었다.


해결

어느정도 규모가 있는 서비스들은 웹 소켓(Web Socket)을 많이 사용해서 그런지 Spring Cloud Gateway(SCG)와 SSE를 쓰면서 발생하는 문제점들을 다룬 자료가 별로 없었다...

 

https://github.com/spring-cloud/spring-cloud-gateway/issues/161

 

Server Sent Events with spring-cloud-gateway · Issue #161 · spring-cloud/spring-cloud-gateway

I am using spring-cloud-gateway v2.0.0.M5 to proxy another spring boot application that uses Server Sent Events (SSE) to send some notifications to the GUI. When I hit the notification server direc...

github.com

결국 공식 Github Issue에서 해결 방법을 찾을 수 있었다.

spring:
  cloud:
    gateway:
      routes:
      - id: spider_route
        uri: ws:${proxy.api.url}
        predicates:
        - Path=/**
        filters:
        - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin # If the back-end server also provides cross-domain support, you need to add this.
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: "*" 
            allowedMethods:
            - POST
            - GET
            - DELETE
            - PUT
            allowedHeaders: "*" #It is not configured to support simple requests across domains such as GET and DELETE. It is essential for complex requests such as POST SSE requests.

위 설정 중 DedupeResponseHeader Filter와 corsConfigurations를 설정해주면 다시 정상적으로 수신이 가능해진다.


설명

적용 코드 설명

DedupeResponseHeader는 게이트웨이 CORS 로직과 다운스트림 로직에서 모두 Access-Control-Allow-Credentials와Access-Control-Allow-Origin 헤더를 추가하는 경우, 중복되는 값을 지워주는 필터이다.

 

corsConfigurations는 해당 경로에 대해 allowedOrigins에서 오는 요청 중 allowedMethods와 allowedHeaders을 만족하는 요청만 CORS를 허용하겠다는 설정이다. (CORS에 대해서는 추후에 작성)

 

공식 문서는 아래를 참고하자.

https://cloud.spring.io/spring-cloud-gateway/reference/html/#the-deduperesponseheader-gatewayfilter-factory

 

Spring Cloud Gateway

This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them

cloud.spring.io

https://cloud.spring.io/spring-cloud-gateway/reference/html/#cors-configuration

 

Spring Cloud Gateway

This project provides an API Gateway built on top of the Spring Ecosystem, including: Spring 5, Spring Boot 2 and Project Reactor. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them

cloud.spring.io


예상 원인

기본적인 CORS 설정은 Front-End나 Back-End 각각 설정이 되어있었지만, 게이트웨이가 라우팅하는 과정에서 SSE같이 비교적 복잡한 요청은 적용이 안되는 모양이다. 정확히 왜 SSE만 안되는지는 알아내지 못해서 아쉽다..


관련 프로젝트

https://github.com/huyeon123/Super-Space

 

GitHub - huyeon123/Super-Space

Contribute to huyeon123/Super-Space development by creating an account on GitHub.

github.com

 

댓글