[스타트업 연계] 웹소켓 통신 서버 프로젝트 간단 회고

2024. 1. 14. 20:37Learn/ASAC

728x90
 

시놀 | Notion

기간: @2023/11/20 ㅡ @2024/01/05 [7주]

softsquared.notion.site

이전 포스팅: [스타트업 연계] 캐싱 프록시 서버 프로젝트 간단 회고 (tistory.com)

팀 생산성

스타트업 연계라고 해서 많은 것을 배울 수 있을 줄 알았는데, SI 업체 취급받아서 기분이 매우 상했다. 하지만 거기에 꺾일 우리가 아니지. 자체적인 세션 제도 및 개발일지 시스템을 운영하여 팀 생산성을 확보하고, 향후 보고서 작성 단계까지 고려한 문서화를 진행한다.

세션 매니징 기반 작업 정리

이 표를 정리하고나니, 7주의 시간이 참 시원섭섭하게 느껴졌다. 스스로 부딪히며 배운 것도 많았고, 느낀 것도 많았다. 프로젝트는 끝날 때마다 뒤돌아보면 항상 아쉬운 점이 보인다. 다음에 그러한 점을 채우려고 노력하는데도 매번 느껴지는 것을 보면 참 성장에는 끝이 없음을 실감한다. 회고에 대한 자세한 내용은 팀 노션에 개별 정리해두었다.

https://www.notion.so/softsquared/cca550df240d46d88551128115c2d86c

구조도

React Native 모바일 환경을 지원하는 웹소켓 통신 구조도 개괄

  특별한 점이라고 한다면, AMQP와 STOMP를 RabbitMQ 플러그인으로 연동하여 구현했다는 점이다. 팀원이 React Native 개발을 Expo 런타임에서 진행하고 있는데, Expo는 Node 내장 함수를 지원하지 않는다. 대다수의 AMQP 라이브러리들이 Node 내장 함수에 의존적으로 구현되어 있어서, AMQP를 사용하려면 자체적으로 프로토콜 명세를 구현해야 하는 상황이 되어버렸다. (배보다 배꼽이 더 큰 상황)

  마침 RN에서 STOMP는 지원한다하여 전환하려 했는데, 살펴보니 RabbitMQ에서 서로 다른 두 프로토콜의 통신을 지원하는 플러그인이 있었다. 이를 활용하여 서버와 메세지 큐는 AMQP 명세로 동작하고, 클라이언트로 전송할 때에는 이를 STOMP 명세로 변환한다. 클라이언트에서 전송할 때에는 브로커에 대해 STOMP로 전달되는데, AMQP 명세에 약속한 방식으로 전달하면 이를 AMQP와 연동할 수 있다.

 

이렇게만 말하면 당연히 이해 못 할테니 궁금한 사람들을 위한 GitHub 링크 투척

ooMia/websocket_js (github.com) (stomp 클라이언트 테스트 코드)

ooMia/websocket_rabbitmq: Basic Websocket Project for real-time voting system (github.com) (amqp 서버 코드)

구체적인 서버 구조도

1. AMQP & Message Durability

  AMQP는 ACK/NACK 명세를 통해 메세지의 송수신 여부를 확인하고, 효율적인 메세지 필터링 기능을 구현할 수 있다. 그러나 STOMP는 프로토콜 자체로는 그런 기능이 없다. 물론 이 방법 이외에도 Time-out이나, 추가적인 메세지 전달로 요청 수행 여부를 확인하는 로직을 구현할 수는 있다. 하지만 절대적인 시간을 기준으로 처리 여부를 판단하는 것과 서버에 장애가 없는 경우를 상정한 로직 구현은 실질적인 장애 상황에서 분명한 기준이 될 수는 없다. AMQP 기반 Pub/sub 시스템의 경우, Pub이 해당 Exchange로 메세지를 넣어두면 Sub은 연결된 Binding에 따라 메세지를 가져간다. Sub는 ACK/NACK를 통해 전송 메세지에 대한 명확한 승인 처리 또한 진행할 수 있다. Pub은 Sub으로부터 ACK를 받지 못한다면 다시 한 번 재전송을 시도할 수도 있다.

이에 대한 자세한 내용은 다음 블로그를 참고하자 [AMQP] - RabbitMQ ACK, Exchange, Route (velog.io)

RabbitMQ 브로커를 둔 Pub/Sub 패턴에서의 ACK

2. Message Durability와 멱등성(Ideompotent)

Idempotent의 개념은 다음 문서에 상세히 기술되어 있다. HTTP는 stateless한 프로토콜이지만, 모든 메서드가 멱등성을 보장하지는 않고, 나아가 safe한 것도 별개로 취급한다. 멱등성 - MDN Web Docs 용어 사전: 웹 용어 정의 | MDN (mozilla.org)\

 

위와 같은 구조에서 Pub의 요청이 Sub에 잘 전달된다면 문제가 없다. 만약 클라이언트가 제대로 전달되지 않은 메세지를 남겨두고 통신을 종료한다면, 문제가 발생한다. 만약 Queue를 Durable:True로 생성한다면, RabbitMQ는 이 메세지를 보존해두었다가 다음 통신 접속 시에 다시 전달한다. 위 시스템에서는 RabbitMQ가 브로커로서 ACK 로직을 구현해두었기 때문에 서버가 별도의 ACK 로직을 구현할 필요가 없다(SoC).

이렇게 동일한 데이터를 받은 이후 ACK를 전송하지 못하면, 다음에 동일한 데이터를 똑같이 받게 된다. 멱등성의 개념은 이 때 필요한데, 프론트 로직은 혹시 모를 오류를 대비하여 동일한 데이터를 받더라도 동일한 결과를 유지할 수 있도록 구현해야 한다.

 

3. STOMP의 명시적 큐 연결 vs AMQP의 익명 큐 연결

Spring Boot를 활용한 구현에 있어서 익명 큐가 더 편하다(SoC, 추상화된 개념만 신경쓰면 됨). 심지어 보안의 측면에서도 최종 연결 채널이 임의의 난수값을 통해 동적으로 생성되는 편이 압도적으로 안전하다.

 

4. RabbitMQ 외부 브로커 사용

향후 확장성을 생각하면 하나의 Spring 서버가 모든 메세지를 처리하는 것은 서버에 대한 부담을 가중시킨다.

RabbitMQ가 처리하도록 구현하면, 단일 서버에 대한 트래픽을 감소시키고, SPOF를 방지하고, 복제를 통한 Scale-out도 매우 간편하게 구현할 수 있도록 한다. 물론 메시지 핸들링 성능도 단일 Spring 서버에 훨씬 뛰어나다. ACK에 대한 구현도 이미 RabbitMQ에 존재하기 때문에 별도로 구현할 필요도 없다.

 

5. Pub/Sub에 대한 별도의 Exchange 생성

앞서 말했듯이 Pub/Sub 모두 동일한 요청에 대해 상태가 동일하게 유지되도록 구현해야 한다. Put 연산의 경우, A를 B로 수정해야 하는데, A, B 상태를 모두 가진 별도의 Dto를 만드는 것은 Payload 크기를 키우고 로직을 복잡하게 만들어 비효율적이라 생각했고, 프론트에서는 ID의 존재를 모른 채로 데이터를 관리할 수 있도록 지원했기 때문에 하나의 Dto를 바탕으로 PUT을 처리할 수 있도록 DELETE/POST만 지원했다. 이렇게 백/프론트 로직을 단순하게 통일시키다보니, 받으면 반영하고 브로드캐스팅... 하면서 동일한 Exchange를 사용하면 무한 루프가 발생했다. 이에 따라 두 채널을 분리했다.

 

개선사항

1. API 사용성 개선

보안적인 측면으로나, API 사용성의 측면에서 ID를 공개할지에 대한 여부는 고민해볼 필요가 있다. Entity 간의 포함 관계를 기반으로 매우 적은 코드로 손쉽게 구현했지만, 프론트에게 ID를 알려주면 프론트의 관리 스트레스는 늘어난다. 왜냐하면 백엔드에게 요청할 때에는 id를 사용해야 하지만, 막상 프론트에 적용할 때, id는 어디에도 적용되지 않는 잉여 속성이기 때문이다. 따라서, 이후 API를 제공할 때에는 id를 제공할지에 대한 여부를 고민해볼 필요가 있다. 중복 속성에 대한 관리를 id 이외의 방법으로 처리할 수 있을지도 고민해볼 필요가 있다. 그 책임을 누가 관리할지도 생각해볼 필요가 있다. 정답이 있는 영역이 아니라 생각하고, 지속적인 경험과 공부가 개선을 만들어주리라 믿는다.

728x90