요약
1. 서버를 분리하면서 SSE를 이용한 실시간 알림 수신 기능에 문제 발생
2. 요청도 정상적으로 갔고, 실제 서버에서 처리도 했지만, 응답만 못받는 채로 타임아웃/재연결 무한반복
3. Spring Cloud Gateway에서 CORS 관련 문제로 응답을 보내주지 않았음
4. application.yml 설정으로 간단히 해결
개요
기존 구현 코드
해당 프로젝트 초기에는 Front-End와 Back-End 분리 없이 하나의 서버에서 모든 과정을 수행했었다.
기능이 점점 추가되면서 각 기능별로 분리하게 됐다.
위 아키텍처로 가기 전에 하나씩 분리하면서 테스트를 통해 기존 기능이 정상적으로 수행하는지 확인했는데, 그 중 실시간 알림 기능이 문제였다.
기능 중 한가지로 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에 대해서는 추후에 작성)
공식 문서는 아래를 참고하자.
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
'Framework > Spring' 카테고리의 다른 글
[Spring JPA, HikariCP] SQLTransientConnectionException 트러블슈팅 (1) | 2022.09.13 |
---|---|
[Spring Security] @AuthenticationPrincipal (0) | 2022.07.11 |
댓글