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

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

JinHwan Kim 2025. 12. 21. 20:52

배경

IoT 서비스 회사에서 백엔드 개발자로 일하고 있다.

기기 사용 환경을 넓히기 위해, 자체 플랫폼 외 LG, 삼성, 네이버, 카카오, KT 등 다른 국내 IoT 플랫폼과의 연동 서비스를 지원한다.

브라우저용 웹 대시보드도 그중 하나이다.

나는 대시보드에서 발생한 제어 요청을 기기에 전달하고, 반대로 기기의 이벤트를 대시보드에 전달하는 중간 다리 역할을 맡는다.

이 글에선 웹 소켓 서버의 관리 어려움과 이를 개선하기 위한 구조 변경 경험을 소개한다.

 

전통적인 웹 소켓 서버 아키텍처

웹 소켓, SSE 같은 연결 유지가 필요한 서비스는 관리가 어렵다.

자원 제한으로 서버의 수평 확장이 불가피하면서도,

분산된 서버 환경에서 연결 정보가 있는 인스턴스를 찾을 수 있는 아키텍처가 필요하다.

 

1. 제한된 자원

서버 한 대에서 연결할 수 있는 소켓(FD) 수에 물리적 한계가 존재한다.

모든 세션 정보를 메모리에 상주시켜야 하므로 동시 접속자 수에 제약이 있다.

 

2. 강한 종속성

클라이언트와 서버가 한번 연결되면, 해당 연결은 최초 수립된 인스턴스에 종속된다.

이로 인해 메시지 전달을 위해선 클라이언트가 연결된 서버를 찾아야 하는 복잡함이 있다.

 

이를 해결하기 위한 대표적인 아키텍처는 다음과 같다.

 

고정 통신 (라우팅 테이블 방식)

클라이언트가 연결된 서버를 기록하는 별도 테이블을 두고, 메시지 전달마다 클라이언트의 연결 서버를 확인하는 방법이다.

구조는 단순하지만, 연결과 끊김에 따라 동적으로 변하는 라우팅 테이블 관리가 어렵다는 단점이 있다.

 

전역 발행 (브로드캐스트 방식)

모든 웹 소켓 서버에 이벤트를 전역 발행하는 방식이다.

수신한 웹 소켓 서버는 자신에게 연결된 클라이언트인지 여부를 확인하고 전송 여부를 결정한다.

서버 간 의존성을 낮춰 확장에는 유리하나, 불필요한 메시지 전송으로 인한 네트워크 비용과 서버 부하가 발생할 수 있다.

 

그룹 발행 (샤딩 방식)

전역 발행 방식의 불필요한 전송과 고정 통신 방식의 의존 문제를 섞는 대안책이다.

미리 정의된 규칙으로 샤드키를 구하고, 이에 따라 연결될 서버 그룹이 결정한다.

전역 발행 방식보다 비용과 서버 부하면에서 효율적이며, 고정 통신 방식보다 유연하다.

대신 해시 키에 대한 서버 그룹이 변경되면 이 또한 또 다른 관리 포인트가 되며,

해시 규칙을 잘못 정의했을 때 특정 서버 그룹에 연결이 쏠리는 핫스팟 문제도 유의해야 한다.

 

실제 운영 이슈

기존에는 브로드캐스트 방식으로 운영하고 있었다.

분명 부하를 못 버틸 상황은 아니다.

다만 웹 소켓 서버와 알림 서버가 분리되어 관리해야 할 서버가 늘었다는 불편함이 있었고,

소켓 연결의 메모리 차지에 대한 걱정으로, 실제 사용 메모리보다 여유로운 인스턴스를 사용한다는 비효율이 있었다.

또 여러 서비스를 거쳐 전달하게 되니, 알림이 유실되었을 때 쫓아가야 할 서비스 홉이 많은 것도 불편했다.

 

 

1. 알림 서버는 브로커로부터 기기 상태 이벤트를 수신하고, 전달 포맷에 맞춰 이벤트를 컨버팅 한다.
2. 유저 정보와 함께 Redis Pub/Sub을 사용하여 웹 소켓 서버에 광역 전달한다.
3. 웹 소켓 서버는 Map { 유저 정보 : 클라이언트 } 을 바탕으로, 연결된 클라이언트에 상태를 전달한다.

 

API Gateway 를 활용한 서버리스 아키텍처

관리 포인트 간소화를 위해, API Gateway WebSocket 를 중심으로 한 서버리스 아키텍처를 도입하였다.

새로운 구조를 통해 다음과 같은 장점을 얻을 수 있었다.

 

1. 자동 확장

앞선 인스턴스의 물리적 연결 수 한계나 메모리 관리에서 벗어날 수 있다.

API Gateway가 연결 관리를 전담하고 수백만 개의 동시 연결이 자동으로 처리한다.

 

2. 사용량 기반 비용

연결 유지 시간과 메시지 수에 따라 사용한 만큼만 지불한다.

 

3. 상태 비저장

연결 상태를 API Gateway 관리하니 더 이상 전송 클라이언트가 어떤 서버에서 관리되는지 탐색하지 않아도 된다.

백엔드는 상태 없이 각 이벤트만 전송하면 되므로 확장에 유리하다.

 

이런 관리의 용이도 좋지만, 단순 비용으로 따져도 전환이 유리할 수 있다.

API Gateway WebSocket는 프리티어로 달마다 첫 100만 분 연결과 100만 개 메시지는 무료이다.

만약 100명의 사용자가 24시간 연결되어 1분에 한 번씩 각각 메시지를 받을 때, 월에 1만 원 수준이다.

오히려 전통적 방식의 서버 운영을 위한 유지비가 더 비쌀 수 있다.

 

Lambda의 역할

Lambda는 Api gateway와 알림 서버 사이에서 세션 정보 관리의 역할을 한다.

인증된 사용자가 소켓 연결에 성공하면, 람다는 사용자 정보와 클라이언트 정보를 레디스에 저장한다.

알림 서버로부터 사용자 정보와 메시지를 수신하면, 람다는 레디스에서 클라이언트 정보를 읽어 게이트웨이로 전달한다.

반대로 게이트웨이로부터 연결 종료 이벤트를 수신하면 레디스에서 해당 매핑 정보를 제거한다.

이때 유저 한 명이 여러 클라이언트로 접속하는 경우를 고려하여 클라이언트들을 Set 으로 담게 된다.

 

연결 유지와 핑퐁

Api gatewa의 기본 설정으로, 10분간 동작이 없는 연결은 해제된다.

클라이언트 단에서는 IDLE로 인식되지 않기 위해, 주기적으로 더미 메시지를 전달하여 연결 해제를 방지한다.

람다는 이 요청에 대답하고, 클라이언트는 반대로 서버의 응답으로부터 연결이 활성화되었는지 다시 체크할 수 있게 된다.

이런 더미 패킷과 응답을 'Ping - Pong' 이라고 한다.

람다는 클라이언트가 { "action": "ping" } 메시지를 보내면, { "action": "pong" }으로 응답함을 정의해야 한다.

 

세션 정보 유지와 TTL

게이트웨이 기본 설정으로 'Maximum Connection Duration'으로 최대 연결 시간을 지정할 수 있다.

기본 값은 2시간인데, 앞선 Ping-Pong 에도 최대 연결 유지 시간은 2시간으로 한정된다.

따라서 레디스에 { 유저 : 연결 정보 } 를 저장할 때는 그 TTL을 2시간보다 조금 더 큰 값으로 한다.

시간이 짧으면 연결 정보가 누락되어 메시지가 유실될 것이고,

너무 크면 이미 끊긴 연결 정보를 불필요하게 유지하는 꼴이 된다.

 

팀 상황에 맞는 최적화 고민

연결 미리 확인
이런 기본적인 구조 외 팀의 특이 사항이 있다면, 너무 많은 기기 이벤트가 들어오고 있다는 것이다.

실제 사용과 다른 기기의 자체 상태 이벤트로, 람다로 전달되는 이벤트 수에 비해 실제 표시되는 수는 매우 적다.

람다로 전달되는 이벤트 중 5%도 소켓 전달에 사용되지 않고, 나머지는 버려지는 비효율이 발생한다.

이런 비효율을 피하기 위해 알림 서버에서 연결 여부를 미리 체크한다.

연결 정보가 있는 경우에만 람다를 호출하는 것으로, 불필요한 레디스 부하를 크게 줄일 수 있었다.

 

Lambda의 Redis 커넥션 관리

람다가 호출 시마다 Redis에 새로 연결되면, Redis 서버의 커넥션 부하로 이어질 수 있다.

람다 실행 컨텍스트에 Redis 클라이언트 객체를 초기화한다.

이렇게 되면 람다의 Warm-start로 미리 생성된 커넥션을 재사용하게 된다.

불필요한 TCP 핸드셰이크를 막아 레디스 부하와 람다 실행 시간을 모두 줄일 수 있었다.

 

Lambda Authorizer

API Gateway에서 사용자 인증 없이 소켓 연결을 수립하면, 다른 사람의 알람을 가로챌 수 있는 큰 문제가 있다.

처음엔 이런 사용자 인증을 게이트웨이 이후에 처리하려고 했다.

게이트웨이의 연결 이벤트에서 JWT 토큰을 확인, 이를 검증하여 연결 정보를 레디스에 저장할지 여부를 결정하는 것이다.

이렇게 하면 알림 서버의 이벤트 전달을 막을 수 있지만, 이미 처리된 연결은 무의미하게 살아있게 된다.

Api gateway의 Authorizer 람다 기능은 연결 요청 시 연결보다 먼저 인증 로직을 수행한다.

검증에 실패한 연결을 미리 거부할 수 있게 되어 불필요한 연결 지속 낭비를 피할 수 있다.

 

성능 테스트

부하 테스트 도구 K6를 사용하여 10,000명 동시 접속 유지 테스트를 진행했다.

10,000개 연결 시도가 모두 성공했으며, 의도대로 최대 동시 접속자 수 10,000명을 달성하였다.

소켓 연결 수립에 걸린 시간은 평균 120ms, 상위 95%도 약 200ms 내에 처리되는 것을 확인하였다.

연결 유지 시간은 스크립트에 설정한 2분 30초 동안 안정적으로 유지되었다.

현재 운영보다 충분히 많은 사용자에서, 빠른 연결 수립 시간과 누락 없는 연결 유지를 테스트할 수 있었다.

 

 

모니터링 지표

Api gateway에서 기본적으로 제공하는 CloudWatch 대시보드도 사용하기 좋았다.

아래는 위 테스트가 진행되었을 시간대의 메시지 수를 표시했다.

그 밖에도 메시지 수, 연결 수, 람다 처리 시간, 클라이언트 단, 람다 단, 게이트웨이 단에서의 에러가 기록된다. 

 

 

 

기존 전통적 방식의 웹 소켓 서버 운영을 위해 만들었던 매트릭 대시보드이다.

Redis 단에선 메시지 발행 수, 발행 시간 지연을, 서버 단에선 커넥션 유지 개수, 이벤트 전달 소요 시간과 성공률을 모니터링했다.

연결 수가 급격히 늘진 않은지, 지연이 있진 않은지 확인이 필요하다.

꼼꼼한 모니터링과 안전한 관리를 위한 고민과 노력이 필요했지만 완벽한 유실 없는 서비스는 불가능했고.

Api Gateway 로 구조를 변경하고는 높은 안정성에 관리 부담이 크게 줄어 도입 두 달이 지난 지금, 유실 문제는 1 건도 없었다.

 

 

 

 

Comments