대규모 실시간 채팅의 병목 현상, 메시지 배치 전송으로 해결하기
이번 글에서는 대규모 트래픽이 발생하는 실시간 채팅 서비스의 성능을 개선한 경험을 공유하고자 합니다. 가상 사용자가 늘어남에 따라 테스트 결과 병목 현상을 겪었고, 이를 해결하기 위해 메시지 전송 구조를 1:1 방식에서 서버 단위 배치(Batch) 방식으로 리팩토링한 과정을 자세히 소개해 드립니다.
실시간은 100ms의 싸움
현대의 웹 서비스에서 '실시간성'은 사용자 경험과 만족도를 결정하는 핵심 요소입니다. 특히 동료와의 협업 툴이나 고객 상담 챗봇 같은 서비스에서 메시지 지연이 100ms만 길어져도 사용자는 '느리다'고 느끼기 시작합니다.
저희가 개발하던 서비스는 병원과 연구소 현장에서 실시간으로 뇌 MRI 결과를 공유하고 토론하는 기능을 포함하고 있었습니다. 의료 데이터라는 특수성과 다자간 협업의 중요성 때문에, 메시지 전송 지연을 최소화하고 네트워크 및 서버 오버헤드를 줄이는 것이 매우 중요한 과제였습니다.
문제 직면: 채팅방 유저가 많아질 수록 리소스 소모가 지수적으로 증가
문제는 동시 접속자 수가 늘어나면서 발생했습니다. 초기 아키텍처에서는 특정 채팅방에 250명의 사용자가 접속한 상태에서 하나의 메시지가 전송되면, 백엔드에서는 250번의 convertAndSendToUser가 개별적으로 호출되었습니다.
이로 인해 다음과 같은 성능 저하가 발생했습니다.
- 메시지 큐 메모리 및 네트워크 패킷의 선형적 증가
- 시스템 과부하로 인한 스로틀링(Throttling) 발생 가능성
기존 구조의 한계: N개의 메시지, N개의 오버헤드
기존 구조는 일반적인 WebSocket 구현에서 흔히 볼 수 있는 직관적인 방식이었습니다.
기존 흐름
- 메시지가 들어오면 Redis에서 채팅방 ID(roomId)를 키로 모든 사용자 목록을 조회합니다.
- 조회된 사용자 목록을 순회하며 각 사용자에게 개별적으로 메시지를 전송(convertAndSendToUser)합니다.
이 구조는 1:1 메시지나 소규모 그룹 채팅에서는 문제가 없지만, 사용자 수(N)가 증가하면 네트워크 호출과 패킷 수가 N에 비례해 폭증하는 명확한 한계를 가집니다. 예를 들어 100명의 사용자가 1초에 한 번씩 메시지를 보내면, 서버는 초당 100 * 100 = 10,000개의 메시지를 브로커로 발행해야 했습니다.
부하 테스트로 확인한 병목 지점
문제를 수치로 증명하기 위해 간단한 성능 테스트를 진행했습니다.
- 시나리오: 250명의 가상 유저가 각자 1~3초당 1개의 메시지를 동시 발행
- 측정 지표: 메시지 큐 메모리 사용량, 네트워크 대역폭(Egress)
결과는 예상대로였습니다. RabbitMQ의 메모리 사용량은 500MB 제한에 거의 도달했고, 네트워크 대역폭 역시 100MB를 넘어서며 시스템이 감당할 수 없는 수준의 부하가 발생함을 확인했습니다.
해결책: 서버 단위 메시지 배치(Batch) 전송
이 문제를 해결하기 위해 '서버 단위 배치 전송' 구조를 고안했습니다. 아이디어의 핵심은 "같은 서버에 접속한 사용자들에게는 메시지를 한 묶음으로 보내자"는 것입니다.
개선된 흐름
- 서버별 그룹핑: WebSocket 연결 시 ws:user:{userId} -> serverId 형태의 사용자-서버 매핑 정보를 Redis에 저장합니다. 메시지 전송 시 이 정보를 이용해 같은 서버에 연결된 사용자끼리 그룹핑합니다.
- 메시지 묶음 생성: 같은 서버 그룹에 속한 사용자 ID 리스트와 원본 메시지를 BatchMessage라는 객체로 묶습니다.
- 한 번에 전송: 그룹핑된 서버별로 단 한 번의 convertAndSend를 호출합니다. 이때 /exchange/chat.{serverId}와 같은 라우팅 키를 사용하여 해당 서버에만 메시지가 전달되도록 합니다.
이 구조에서는 메시지 전송 요청이 사용자 수(N)가 아닌, 메시지를 수신할 서버 수(S)만큼만 발생합니다. 서버가 10대라면, 100명의 사용자에게 메시지를 보내더라도 단 10번의 전송 요청으로 끝나는 것입니다.
개선 결과: 눈에 띄게 안정화된 시스템
리팩토링 후 동일한 조건의 부하 테스트를 다시 수행했습니다. 결과는 놀라웠습니다.
- 메시지 전송 횟수: 250회 → 최대 서버 수 2(S) 만큼 감소
- 메모리 및 네트워크 사용량: 매우 안정적인 상태 유지
이러한 개선 덕분에 동일한 하드웨어 리소스에서 훨씬 더 많은 동시 접속자를 감당할 수 있게 되었습니다. 실제로 250명을 넘는 동시 접속 부하 테스트에서도 안정적인 응답 시간을 유지하는 것을 확인했습니다.
배치 전송의 기대효과 및 장점
이번 리팩토링을 통해 얻은 이점은 명확합니다.
- 메시지 수 감소: N개의 1:1 전송이 S개의 그룹 전송으로 바뀌면서(S ≪ N) 메시지 브로커의 부하가 획기적으로 줄었습니다.
- 네트워크·메모리 부하 절감: 메시지 직렬화(Serialization) 및 I/O 호출의 중복이 최소화되어 시스템 자원을 효율적으로 사용하게 됐습니다.
- 수평 확장(Scale-out) 용이성: 향후 트래픽 증가에 따라 서버 노드를 추가하더라도, 서버별 그룹핑 로직은 그대로 유지되므로 손쉽게 확장이 가능합니다.
결론적으로, 더 적은 리소스로 더 많은 트래픽을 안정적으로 처리할 수 있는 기반을 마련했습니다.
참고 영상
https://youtu.be/_F6k0tg8ODo?si=84OoqmsDIBtxpUVA
'스프링' 카테고리의 다른 글
MSA환경에서 추상화를 통한 Test 환경 구축하기 (0) | 2025.01.29 |
---|---|
AOP로 중복 요청 제어하기 (1) | 2024.12.29 |
JPA에서 처리하는 1 : N 관계 (2) | 2024.09.11 |
Spring Security 기술로 인증/인가 커스텀하기 (0) | 2024.08.25 |
스프링과 JWT(Json Web Token) 기술을 이용한 인증과 인가 구현 (0) | 2024.08.06 |