웹소켓 서버 구조 개선 : Api Gateway WS를 사용한 서버 리스 전환 본문

웹소켓 서버 구조 개선 : Api Gateway WS를 사용한 서버 리스 전환

JinHwan Kim 2025. 12. 21. 20:52

배경

최근 프로젝트로 서버에서 클라이언트로 데이터를 전달해야 하는 요구사항이 생겼다. 좀 더 자세히는 한 B2B 서비스에 우리 회사 카메라 기기의 스트리밍을 제공해야 하는데, 이때 사용자 페이지와 기기 사이의 중개 서버 개발을 맡았다. WebRtc를 수립을 위해선 기기와 사용자 페이지 간 네트워크 경로 후보지가 서로 오가야 했고, 서버에서 클라이언트로 데이터 전달에 WebSocket 혹은 SSE를 사용해야 했다.

 

고질이었던 기존 WebSocket 서버 관리 방법을 다시 한번 고민할 수 있는 기회가 되었다. 그 과정에서 AWS의 API Gateway WebSocket을 사용하면 웹 소켓 서버를 직접 관리하지 않아도 됨을 찾을 수 있었다. 생각보다 저렴한 비용에 편한 모니터링, 무엇보다 거의 없다시피 한 관리 포인트로 안정적인 확장이 가능함에 놀랐다.

 

이 글에서는 내가 느낀 기존 전통적 방식의 웹 소켓 서비스 운영의 제약을 소개하고, 새로 확인한 API Gateway WebSocket의 아키텍처와 개선 포인트를 정리한다.

 

전통적인 방식의 웹 소켓 서버 운영

웹 소켓, SSE 같은 연결 유지가 필요한 구조를 관리하는 게 어렵다고 느낀다.

 

- 제한된 자원 : 배포 환경에 따라 연결에 사용할 수 있는 소켓 수가 제한적이다. 또 연결된 세션 객체를 메모리에 상주시켜야 한다. 결국 서버 한대가 수용할 수 있는 동시 접속자 수에 물리적인 한계가 존재한다. 

 

- 강한 종속성 : 클라이언트와 서버가 연결을 맺게 되면, 해당 클라이언트로의 이벤트 전달은 연결을 최초 수립한 서버를 통해서만 처리할 수 있다. 문제는 앞선 제한된 자원과 고가용성의 문제로 서버를 단일로만 유지할 수 없다는 것이다. 만약 유저 1이 서버 A와 연결을 수립한 상태에서, 서버 B에서 유저 1의 이벤트가 발생한다면 서버 B는 유저 1의 연결 정보가 있는 서버를 찾아 이벤트를 전달해야 한다.

 

이런 자원의 제한으로 웹 소켓 서비스의 수평 확장은 불가피하고, 그로 인한 분산 환경에서 연결 정보가 있는 서비스를 찾을 수 있는 아키텍처가 필요했다. 내가 알고 있는 대표적인 아키텍처 패턴은 다음 두 가지이다.

 

1. 고정 통신

알림 발생 서비스에서 어떤 웹 소켓 서버가 어떤 클라이언트의 연결을 맺고 있는지 알고 직접 통신하는 구조이다. 구조에 따라 연결 정보를 기록한 테이블을 두기도 하고, LB에서 라우팅을 고정하는 꼴일 수도 있다. 이 방식은 이벤트가 바로 전달되기에 불필요한 네트워크 통신이 발생하지 않는다. 대신 전달 서비스 엔드포인트 같은 인프라 정보에 직접 의존이 생기고, 동적으로 변하는 라우팅 방법에 테이블 관리도 쉽지 않다.

 

 

2. 전역 발행 

이벤트를 모든 웹 소켓 서버에 던지는 방법이다. 모든 웹 소켓 처리 서버에서 모든 이벤트를 수신하고, 본인에게 연결된 유저 정보인지를 필터링한다. Redis Pub/Sub과 같은 중간 브로커를 사용하기에 서버 간 의존도가 낮아져 서버 스케일링에 유리하고, 또 라우팅 테이블을 직접 관리해야 하는 수고스러움도 없다. 대신 모든 메시지가 전부 전달되기에 각 서버의 부하로 이어질 수 있고, 네트워크 트래픽 비용이 발생한다. 이럴 땐 아래 그림처럼 토픽 자체도 여러 개 분리해서 브로드캐스팅의 범위를 나누는 샤딩을 적용할 수 있다.

 

 

3. 모니터링

각 방법에 장단점이 명확하다. 그래서 다른 서비스들보다도 더 꼼꼼한 모니터링 대시보드가 필요하다. 아래는 Redis pub/sub을 사용한 전역 발행 구조에 만들어 본 매트릭 대시보드이다. 레디스 단에선 메시지 발행 수, 발행 시간 지연을, 웹 소켓 처리 단에선 커넥션 유지 개수, 전체 커넥션이 처리하고 있는 IoT 기기 수, 이벤트 전달 소요 시간과 성공률을 모니터링한다. 사실 이런 모니터링에도 연결 수나 이벤트가 급격히 늘었을 때 전달 지연이 발생하거나 서비스 장애가 있진 않을지 관심을 갖고, 적절한 타이밍에 확장할 수 있도록 유의해야 한다.

 

 

 

서버 리스 방식의 운영

이번에는 AWS API Gateway WebSocket과 서버 리스 조합으로 웹 소켓을 다루는 방법을 소개한다.

 

1. 아키텍처 

API Gateway WebSocket + Lambda를 사용하여 웹 소켓 서비스 구조이다. 

초록색 : 알림이 발생하는 서비스로 Spring boot로 개발되었다.
빨간색 : 유저 정보와 커넥션 정보를 매핑하는 Session table로 Redis를 사용한다. 
파란색 : API Gateway와 백엔드 서비스 사이에서 메시지를 중개한다.

 

이를 통해 얻을 수 있는 구조적 장점은 다음과 같다.

- 스케일링 : 연결 수를 개발자가 신경 쓰지 않아도 된다. AWS 측에서 자동 확장한다.

- 무상태성 : 연결과 상태는 Api gateway에서 관리한다. 이를 사용하는 백엔드 서비스는 무상태가 되어 관리에 용이하다.

- 비용 : 연결 지속 시간과 메시지 수에 따라 사용한 만큼만 지불한다. 고정 서버 유지비가 불필요하다.

 

 

 

2. 처리 흐름

아키텍처만큼이나 처리 흐름이 깔끔하고 의도도 명확하다. 클라이언트 단에서 최초 연결 시 WS 게이트웨이에 인증 정보가 담긴 JWT을 전달한다. 중앙 Lambda에선 이 토큰을 검증하고, 그 안에서 사용자 정보를 꺼낸다. 게이트웨이에서 전달된 ConnectionId와 사용자 정보를 Session 테이블에 매핑하여 기록해 둔다.

 

알림 발생 서비스에서는 전달해야 하는 사용자가 연결되어 있는지, 어떤 연결 정보를 갖는지 전혀 모른다. Spring boot에서 사용할 수 있는 AWS SDK로 Lambda에 직접 메시지를 전달할 수 있다. Lambda에선 레디스를 읽어 사용자 정보에 해당하는 ConnectionId을 모두 읽어온다. Lambda에서도 마찬가지로 AWS SDK (boto3)를 사용해서, ConnectionId와 메시지로 함수로 호출하면 발송 완료다.

 

참고로 연결을 제외한 그 이후의 클라이언트 to 서버 통신에서 JWT를 매번 전달하고 검증하지 않아도 된다. 첫 인증 이후 연결이 유지된 클라이언트로부터의 메시지는 게이트웨이로부터 ConnectionId와 함께 Lambda에 전달된다. 이 Lambda는 AWS IAM으로 지정한 리소스에서만 호출할 수 있기에 정의한 API 게이트웨이 리소스가 보낸 연결 정보를 신뢰할 수 있다. 레디스에 해당 연결 정보가 있는지 확인하는 것으로 해당 메시지가 연결 시에 JWT 인증이 되었는지 여부를 검증할 수 있다.

 

3. 연결 유지와 Ping

Api Gateway WebSocket의 주요 설정과 기본 값은 다음과 같다.

- Idle Connection Timeout : 10분
- Maximum Connection Duration : 2시간
- Maximum Frame Size : 128 KB
- New connections/sec : 500

 

10분간 아무런 동작이 없는 연결은 해제된다. 정상 동작하고 있는 클라이언트 단에서는 IDLE로 인식되지 않기 위해 주기적으로 'Ping' 메시지를 전송하여 연결 해제를 방지한다. 반대로 클라이언트에선 이 Ping에 대한 답이 응답되는지를 확인하여 서버의 연결이 활성 상태임을 확인할 수 있다. 위 Api Gateway WS를 사용하는 구조라면, Lambda에서 게이트웨이로부터 수신한 메시지가 'Ping'이라면 'Pong' 메시지를 반환하는 규칙을 정의하여 클라이언트에 서버가 활성화 상태임을 전달할 수 있다.

 

이런 Ping - Pong 에도 최대 연결 유지 시간은 2시간으로 한정된다. 따라서 레디스에 연결 정보를 저장할 땐 TTL을 2시간보다 적당히 크게 잡으면 된다. 2시간보다 짧으면 연결이 유지되어 있는데도 { 유저 - 연결 } 정보가 없어 메시지를 전송할 수 없게 되고, 반대로 2시간보다 너무 크면 이미 삭제된 연결 정보를 비효율적으로 유지하는 꼴이 된다.

 

Lambda를 동시에 활성화시킬 수 있는 개수가 지정되어 있음을 주의한다. 기본 값은 계정당 1000개인데, AWS에 요청하여 그 설정 수를 늘릴 수 있다. 만약 이 제한을 인지하지 못하고 초과한다면, 요청을 더 이상 처리할 수 없어 병목 현상이 발생하게 된다. 대신 이 Lambda의 동시 활성화 수와 게이트웨이의 WebSocket 동시 연결 개수는 구분해야 한다. 메시지 처리에 사용되는 Lambda의 동시 실행 개수이지, 전체 커넥션 동시 유지 개수가 1000개인 것은 아니다.

 

4. 동시 연결 테스트와 모니터링

K6를 사용하여 만 명의 동시 접속 유지를 테스트해 보았다. 더 많은 수를 확인하고 싶었지만 테스트로 사용한 로컬 기기의 FD 한계로 우선 만개로 진행했다.

 

아래 결과를 보면 10000개의 연결 시도(checks)가 모두 성공하여, 최대 동시 접속자 수(vus_max)가 의도대로 만개이다. ws_connecting는 웹 소켓 연결 수립 시간을 의미한다. 평균 120ms, 상위 95%의 연결도 약 200ms에 처리되었음을 볼 수 있다. ws_session_duration은 연결 유지 시간을 표시한다. 2분 30초 동안 유지하고 연결을 해제하는 의도대로 동작했음을 확인할 수 있다.

 

 

Api gateway에서 기본적으로 제공하는 CloudWatch 로그도 사용하기 좋았다. 아래는 위 테스트가 진행되었을 시간대의 메시지 수를 표시했다. 그 밖에도 메시지 수, 연결 수, Lambda의 처리 시간, 클라이언트의 에러, Lambda에서의 에러, Api gateway 단에서의 에러가 기록되고, 아래처럼 쿼리 할 수 있다.

 

 

정리

이슈 :

  - B2B 카메라 스트리밍 지원을 위한 서버 to Client 데이터 전달에 WebSocket, SSE 필요

  - 기존 전통적인 방식의 WebSocket 서버 관리의 어려움과 그에 따른 API Gateway WS + 서버 리스 구조 학습

확인 : 

  - 기존 WebSocket 서버 운영의 어려움 : 1. 제한된 자원, 2. 연결 서버에 종속된 통신 

  - 연결 정보를 찾기 위한 대표적인 아키텍처 : 1. 라우팅 테이블을 사용한 직접 전달, 2. Redis Pub/Sub을 사용한 전역 전달

  - Api Gateway WS 아키텍처 소개 : 1. 서버 리스, 2. 스케일링, 3. 비용

  - 연결 유지를 위한 PIng, Pong

  - 동시 연결 유지 테스트와 CloudWatch 대시보드

 

 

Comments