이중화 된 서버에서 WebSocket 연결 관리

꾸준하게 20년·2023년 5월 2일
0

개발환경

  • spring-boot 2.7.12
  • open-jdk 11
  • redis

개발해야 하는 기능은

  1. api 요청이 발생하면
  2. 서버와 연결된 웹 소켓을 통해 대상 장치까지 요청 값을 전달
  3. 다시 웹소켓을 통해 대상 장치로 부터 응답값을 받고
  4. api 요청에 대한 응답값을 리턴하는 것이다.

서버의 구성은 nginx(로드밸런싱) -> 서버1, 서버2 로 되어 있는 상황이다.

기능 구현을 위해 사전에 고려해야 하는 것은

서버1에서 api 요청을 수신 받더라도 실제 웹 소켓과 연결된 곳은 서버2 일 수 있다.
결국 서버1과 서버2 사이에서 요청과 응답을 중계하는 단계가 필요하다.

중계를 하는 방식은 redis pub/sub 사용하였다. 이 방식을 선택한 이유는 아래와 같다.
1. 이미 redis 를 사용하고 있었고.
2. 요청/응답에 대한 값을 저장 할 필요 없이 중계하는 목적에 부합.
3. 추후 서버의 수평 확장(scale-out)이 발생하더라도 서비스 로직에 대한 수정 사항이 발생하지 않는 것.

요청과 응답에 대한 처리는 이벤트 방식으로 작성하였다.

org.springframework.context.ApplicationEventPublisher 클래스를 사용하면 이벤트를 발행하고 처리하는 과정을 쉽게 개발 할 수 있다.
이벤트를 처리하는 리스너는 org.springframework.scheduling.annotation.Async 어노테이션을 사용하여 비동기로 처리 할 수 있도록 설정하고 별도 스레드 풀을 지정하여 병목 현상이 발생 하지 않도록 개선하였다.

api 요청/응답 또한 비동기로 처리하기 위해

org.springframework.web.context.request.async.DeferredResult 를 사용하였다.

api 요청이 서버1로 들어와서 redis 통해 중계되어 서버2로 전달되고, 서버2에서는 자신한테 연결된 웹 소켓을 통하여 요청값을 보낸다.
이후 서버2는 웹 소켓으로 부터 응답값을 받고, 다시 redis 통해 서버1로 전달해야 한다.

이 과정을 매끄럽게 처리하려면 서버1은 서버2로 요청을 전달하기 전에 현재 요청에 대한 정보를 저장해야 한다.
저장한 값은 서버2를 통해 응답값을 받았을때 사용되며, 최종 api 요청에 대한 응답값을 처리하는 용도로 사용 된다.

현재 요청에 대한 정보를 저장 할때 고려한 부분은

  1. 요청에 대한 구분값은 UUID 를 사용.
  2. 오류 발생을 대비한 예외처리(timeout 등)

api 요청에 대한 인증은 토큰 방식으로 처리하고 있었고, 요청시 트랜잭션ID 항목이 존재하는데 이 값의 형식이 UUID 였다.
중복되지 않으며 추후 로그 분석을 통해 추적에도 용이한 부분이 있다고 판단하여 내부적으로 조합하여 사용하였다.
요청 정보의 저장은 java.util.concurrent.ConcurrentHashMap 를 사용하였다.

DeferredResult 객체의 onTimeout(), onError() 를 통해 예외 상황에 대한 처리를 지정하고, 해당 예외가 발생 할 경우 setErrorResult() 메소드를 호출하여 응답을 처리하였다.

후기

  • redis pub/sub 을 사용하여 중계하는 방식을 선택하였는데 다음에는 메세지 큐를 사용해 보자.
  • spring webflux 스택을 사용하여 비동기 처리를 구현해 보자.
profile
앞으로 1년

0개의 댓글