들어가며
SpeakNote를 개발하면서 가장 먼저 부딪힌 문제는, 실시간 음성 입력을 서버에서 어떻게 받아야 안정적인 구조를 만들 수 있을지에 대한 고민이었습니다. 일반적인 웹 서비스처럼 요청이 들어오고 응답을 반환하는 구조와 달리, 음성 스트리밍은 사용자가 말을 하는 동안 아주 짧은 오디오 조각들이 끊임없이 서버로 전달되는 형태이기 때문에, 서버 입장에서는 “요청을 처리한다”기보다는 “흐름을 받아낸다”에 가깝습니다. 이 흐름을 제대로 설계하지 않으면, 서버는 생각보다 빠르게 병목 상태에 빠지게 됩니다.
이 글에서는 SpeakNote에서 Spring Boot 기반 WebSocket 서버를 어떻게 설계했고, 왜 그런 구조를 선택했는지를 중심으로 정리해보려 합니다.
1. 실시간 음성 입력의 특성과 WebSocket 선택 이유
실시간 음성 입력은 서버 입장에서 보면 “처리 요청”이라기보다 “지속적인 데이터 스트림”에 가깝습니다. 사용자가 말을 시작하는 순간부터 멈출 때까지, 아주 짧은 오디오 데이터가 밀리초 단위로 끊임없이 유입됩니다. 이런 입력을 HTTP 요청–응답 모델로 처리하는 것은 구조적으로 무리가 있었고, 연결을 유지한 상태에서 데이터를 주고받을 수 있는 WebSocket이 자연스러운 선택이었습니다.
하지만 WebSocket은 입력을 빠르게 받는 데에는 적합한 반면, 입력 속도를 서버가 제어하기는 매우 어렵다는 단점도 함께 가지고 있습니다. 사용자가 말을 빠르게 하면 할수록 서버로 들어오는 데이터 양은 즉각적으로 증가하고, 서버가 이를 처리하지 못하는 순간 지연은 바로 체감됩니다. 따라서 WebSocket을 사용하되, 그 위에 어떤 구조를 얹을지가 핵심 과제가 되었습니다.
WebSocket이란?
WebSocket은 클라이언트와 서버가 연결을 유지한 상태에서 양방향 통신을 할 수 있도록 하는 프로토콜입니다.
이 글에서는 WebSocket의 개념 자체보다는, 실시간 입력을 서버에서 어떻게 안전하게 받느냐에 초점을 맞추고 있으며, WebSocket 프로토콜의 상세 동작은 별도의 글에서 다루겠습니다.
2. WebSocket 서버가 병목의 시작점이 되기 쉬운 이유
WebSocket은 실시간 입력에 적합하지만, 동시에 가장 먼저 병목이 발생하기 쉬운 지점이기도 합니다. 이유는 명확합니다. WebSocket은 연결 유지형 통신이기 때문에, 클라이언트가 보내는 데이터 속도를 서버가 직접 제어하기 어렵습니다. 사용자가 말을 빠르게 하면 할수록, 서버에는 밀리초 단위로 오디오 데이터가 계속 유입됩니다.
이때 서버가 입력을 처리하는 속도가 유입 속도를 따라가지 못하면, 지연은 즉시 누적됩니다. 더 문제인 점은, 이 지연이 단순히 “느려진다” 수준이 아니라, 경우에 따라 전체 연결이 불안정해질 수 있다는 점입니다. 따라서 WebSocket 서버는 단순히 기능을 구현하는 수준을 넘어서, 입력 폭주(backpressure)를 어떻게 흡수할 것인가를 구조적으로 고민해야 하는 영역이었습니다.
3. SpeakNote의 WebSocket 서버 설계 목표와 역할

SpeakNote에서 WebSocket 서버를 설계하면서 가장 먼저 정한 목표는 명확했습니다.
WebSocket 서버는 절대 무거운 처리를 담당하지 않는다는 원칙입니다.
음성 인식, 텍스트 누적, AI 호출과 같은 작업을 WebSocket 수신 로직 안에서 처리하기 시작하면, 어느 한 단계에서 지연이 발생하는 순간 입력 자체가 막히게 됩니다. 그래서 WebSocket 서버는 오직 다음 역할만 수행하도록 제한했습니다.

(즉 , 연결 유지형 프로토콜임 -> 연결이 유지되는 동안 한 지점에서 블로킹이 발생하면, 전체 체인에 영향을 받기 때문에 처리 계층으로 사용한게 아니라 경계 계층으로 사용 )
- 클라이언트로부터 오디오 데이터를 안정적으로 수신
- 수신된 데이터를 즉시 내부 큐로 전달
- 다음 처리 단계와의 경계 역할만 수행
이렇게 역할을 최소화함으로써, WebSocket 서버는 입력 계층으로서 최대한 단순하고 견고한 구조를 유지하도록 설계했습니다.
SpeakNote에서는 WebSocket 서버를 하나의 처리 노드로 보지 않고, 외부 입력을 내부 파이프라인으로 안전하게 전달하는 경계 계층으로 정의
이 계층에서 병목이 발생할 경우, 이후 단계의 지연 여부와 관계없이 입력 자체가 차단되기 때문에 WebSocket 서버는 가능한 한 짧은 실행 경로와 최소한의 책임만을 가지도록 설계하는 것이 핵심이라고 판단
WebSocket 프로토콜 자체는 생산자(클라이언트)의 전송 속도를 소비자(서버)의 처리 속도에 맞춰 조절해 주지 않기 떄문에 SpeakNote에서는 이 특성을 고려해서 서버가 처리할 수 있는 속도를 초과하는 입력이 들어오는 상황을 전제로 설계하였습니다.
4. 세션 단위 파이프라인 구조와 입력 분리
SpeakNote의 WebSocket 구조에서 가장 중요한 설계 포인트는 세션 단위로 파이프라인을 완전히 분리했다는 점입니다. 사용자가 WebSocket으로 접속하면, 서버 내부에는 해당 사용자만을 위한 세션 컨텍스트가 생성됩니다. 이 세션은 자신만의 Inbound Audio Queue와 이후 처리 단계를 독립적으로 보유합니다.

이 구조를 선택한 이유는, 특정 사용자의 입력이나 처리 지연이 다른 사용자에게 전파되지 않도록 하기 위함이었습니다. 만약 모든 사용자의 오디오 입력을 하나의 공용 큐에서 처리했다면, 한 세션에서 발생한 병목이 곧바로 전체 서비스 품질 저하로 이어졌을 것입니다.
세션 단위 파이프라인 구조에서는 지연이 항상 “해당 세션 내부”에서만 흡수됩니다. 입력이 빠른 사용자, 처리 시간이 긴 사용자, 네트워크 상태가 불안정한 사용자가 서로에게 영향을 주지 않도록 구조적으로 차단한 셈입니다.
5. Spring Boot WebSocket 처리 흐름 상세
Spring Boot WebSocket 핸들러는 바이너리 메시지를 처리하는 방식으로 구현되어 있습니다. 클라이언트에서 전송되는 오디오 데이터는 바이너리 형태로 전달되며, 서버는 이를 수신하자마자 해당 세션의 Inbound Queue에 적재합니다. 이 과정에서 중요한 점은, WebSocket 핸들러 내부에서 어떠한 비즈니스 로직도 수행하지 않는다는 점입니다.
즉, WebSocket 핸들러는 “받아서 넘긴다”는 역할에만 집중합니다. 이후 오디오 데이터의 소비, 음성 인식 스트림 전송, 텍스트 누적과 같은 작업은 모두 WebSocket 외부의 워커 또는 후속 파이프라인에서 처리됩니다. 이로 인해 WebSocket 서버는 입력 부하에 최대한 강한 구조를 유지할 수 있었고, 처리 지연이 발생하더라도 입력 자체가 막히는 상황을 피할 수 있었습니다.
Inbound Queue란?
Inbound Queue는 WebSocket으로 수신된 오디오 데이터를 임시로 저장하는 버퍼 역할을 합니다.
큐의 크기 조절, 소비 속도 제어와 같은 세부 전략은 별도의 포스트에서 다룰 예정입니다.
InboundAudioQueue는 단순한 비동기 처리를 위한 큐가 아니라, WebSocket 입력과 내부 처리 파이프라인 사이의 완충지대 역할을 수행하는 친구!
6. 이 구조로 줄일 수 있었던 병목과 남은 과제
이와 같은 WebSocket 서버 구조를 통해 SpeakNote는 실시간 음성 입력 단계에서 발생할 수 있는 병목을 상당 부분 줄일 수 있었습니다. 입력 폭주가 발생하더라도 WebSocket 서버가 먼저 무너지는 일은 없었고, 지연은 항상 세션 내부에서 국소적으로 처리되었습니다.
다만, WebSocket 이후 단계에서의 병목, 예를 들어 음성 인식 스트리밍이나 AI 주석 생성 단계에서의 지연은 또 다른 문제 영역입니다. 이러한 부분은 WebSocket 서버의 책임 범위를 넘어서는 영역이기 때문에, 이후 글에서는 gRPC 기반 STT 스트리밍 구조와 Python 백엔드의 작업 분리 설계에 대해 이어서 정리해볼 예정입니다.
다음 글
- [SpringBoot - gRPC] 실시간 STT 스트리밍과 세션별 gRPC 스트림 분리 (Project: SpeakNote)
- [Architecture - Real-time] 세션 단위 파이프라인으로 병목을 국소화한 이유 (Project: SpeakNote)