1. 왜 WebSocket 뒤에 gRPC 스트리밍이 필요한가
SpeakNote는 사용자의 음성을 실시간으로 받아 텍스트로 변환하고, 그 결과를 다시 요약·주석 형태로 사용자에게 되돌려주는 시스템입니다. 이 과정에서 가장 중요한 요구사항은 “실시간성”과 “연속성”이었습니다. 사용자는 일정 간격으로 끊임없이 음성을 전송하고, 서버는 그 흐름을 끊지 않은 채 텍스트 결과를 받아야 합니다.
브라우저와 서버 간의 통신은 WebSocket이 담당하지만, 음성 인식 자체는 외부 STT 엔진에 위임됩니다. SpeakNote에서는 Google Speech-to-Text의 Streaming API를 사용했고, 이 API는 gRPC 기반의 양방향 스트리밍 방식으로 동작합니다. 즉, 서버는 오디오 데이터를 지속적으로 전송하면서 동시에 인식 결과를 비동기로 수신해야 합니다.
이 지점에서 핵심 설계 문제가 발생합니다.
“여러 사용자의 실시간 음성 스트림을 하나의 STT 연결로 처리할 수 있는가?”
결론부터 말하자면, SpeakNote에서는 그렇게 설계하지 않았습니다.
2. SpeakNote의 STT 처리 흐름 개요
SpeakNote의 전체 흐름은 다음과 같이 구성됩니다.
브라우저는 WebSocket을 통해 일정 길이의 PCM 오디오 청크를 서버로 전송하고, Spring Boot 서버는 이를 세션 단위로 분리하여 수신합니다. 이후 각 세션은 자신에게 할당된 STT 스트림을 통해 Google STT 서버로 오디오를 전달하고, partial 또는 final transcript 형태의 응답을 다시 수신합니다.

이 과정에서 중요한 점은 WebSocket과 gRPC가 직접 연결되어 있지 않다는 점입니다. WebSocket은 단순히 오디오 입력을 전달하는 역할을 담당하고, 실제 STT 스트리밍은 세션 내부에서 별도의 워커를 통해 수행됩니다. 이 구조를 통해 WebSocket 서버는 STT 처리 지연으로부터 자유로워질 수 있습니다.
3. 세션별 gRPC 스트림 분리 설계
SpeakNote에서 가장 중요한 설계 결정 중 하나는 “세션마다 gRPC 스트림을 독립적으로 유지한다”는 점이었습니다. 사용자가 WebSocket으로 접속하면 서버 내부에는 해당 사용자를 위한 SessionCtx가 생성되고, 이 컨텍스트는 Inbound Queue, Text Buffer, Outbound Queue를 포함한 독립적인 처리 파이프라인을 가집니다.
이 SessionCtx는 단순한 데이터 컨테이너가 아니라, 하나의 STT 스트림과 1:1로 대응되는 논리적 단위입니다.
즉, SessionCtx A는 Google STT Stream A와 연결되고, SessionCtx B는 Stream B와 연결되기 때문에 이 구조에서는 한 세션의 음성 입력이 다른 세션의 STT 처리 흐름과 섞이지 않습니다.
만약 모든 사용자의 오디오를 하나의 gRPC 스트림으로 처리했다면, 한 사용자의 발화 지연이나 네트워크 문제, 혹은 STT 응답 지연이 전체 서비스에 영향을 주었을 것입니다.
SpeakNote는 이러한 전파형 병목을 구조적으로 차단하기 위해 세션 단위 스트림 분리 방식을 선택했습니다.

4. partial / final transcript를 고려한 연결 구조
Google STT Streaming API는 음성 인식 결과를 partial transcript와 final transcript로 나누어 반환합니다. partial 결과는 아직 확정되지 않은 중간 인식 결과이며, final 결과는 해당 구간의 발화가 종료되었음을 의미하는 확정 텍스트입니다.
SpeakNote에서는 이 두 결과를 동일하게 취급하지 않습니다. gRPC 스트림으로부터 수신되는 STT 응답은 모두 세션의 Text Buffer에 누적되지만, 실제 요약 및 주석 생성 파이프라인은 final transcript를 기준으로 동작하도록 설계했습니다. 이를 통해 불안정한 중간 결과가 요약 품질에 영향을 주는 것을 방지할 수 있었습니다.
partial / final transcript를 어떻게 구분하고, 어느 시점에 어떤 처리를 수행할지에 대한 전략은 실시간 시스템에서 매우 중요한 설계 포인트입니다. 이 부분은 STT 스트리밍 데이터의 “의미적 상태 관리”와 깊이 연관되어 있기 때문에, 본 글에서는 구조적 연결까지만 다루고, 실제 처리 전략과 트레이드오프에 대해서는 별도의 글에서 자세히 설명할 예정입니다.
5. 이 구조가 병목을 막는 방식
이러한 세션별 gRPC 스트림 분리 구조 덕분에, STT 처리 지연은 항상 해당 세션 내부에서만 흡수됩니다. 특정 사용자의 발화가 길어지거나 STT 응답이 지연되더라도, 그 영향은 다른 세션으로 전파되지 않습니다.
또한 WebSocket 서버는 gRPC 호출을 직접 처리하지 않기 때문에, STT 스트림의 상태와 무관하게 안정적으로 입력을 수신할 수 있습니다. WebSocket → SessionCtx → gRPC Stream이라는 단계적 분리는 실시간 시스템에서 자주 발생하는 backpressure 문제를 구조적으로 완화하는 역할을 합니다.

사용자는 3 초 내외 PCM 청크를 WebSocket 으로 전송하고, 서버는 세션 전용 큐·워커를 통해 STT 스트리밍 및 비동기 요약을 수행한 뒤 결과를 WebSocket 으로 즉시 푸시. ‘par/loop/alt’ 표기는 병렬·반복·분기 흐름을 의미
5.1 세션 단위 실시간 주석 생성 시퀀스 설명
위 시퀀스 다이어그램은 SpeakNote에서 하나의 세션이 생성된 이후, 사용자의 음성 입력이 어떻게 실시간 주석으로 변환되어 다시 화면에 반영되는지를 단계별로 나타낸 것입니다. 이 다이어그램은 단순히 기능 호출 순서를 나열한 것이 아니라, 세션 단위 파이프라인이 실제 런타임에서 어떻게 분리되어 동작하는지를 시각적으로 보여주는 핵심 자료입니다.
먼저 사용자는 브라우저에서 WebSocket 연결을 통해 일정 길이의 PCM 오디오 청크를 지속적으로 서버로 전송합니다. 이 오디오 입력은 WebSocket Handler에 의해 수신되며, 이 시점에서 서버는 해당 연결을 고유한 세션 컨텍스트(SessionCtx)에 바인딩합니다. 이후 들어오는 모든 오디오 데이터는 공용 큐로 흘러가지 않고, 해당 세션 전용 Inbound Queue에만 적재됩니다.
세션 내부에서는 전용 워커가 Inbound Queue를 지속적으로 폴링하며 오디오 청크를 소비하고, 이를 해당 세션에 대응되는 Google STT gRPC 스트림으로 순서대로 전달합니다. 이때 중요한 점은 WebSocket 처리 흐름과 STT 스트리밍 흐름이 동일한 실행 경로에 묶여 있지 않다는 점입니다. WebSocket은 입력을 전달하는 역할에만 집중하고, 실제 음성 인식 처리는 세션 내부 워커와 gRPC 스트림을 통해 비동기적으로 수행됩니다.
Google STT 서버로부터의 응답은 partial transcript와 final transcript 형태로 비동기 수신되며, 이 결과는 다시 세션의 Text Buffer에 누적됩니다. partial transcript는 사용자의 발화 중간 상태를 빠르게 반영하기 위한 용도로 활용되고, final transcript는 문장이 종료되었음을 기준으로 이후 요약 및 주석 생성 단계로 전달됩니다. 이 과정에서 발생하는 STT 지연이나 응답 편차는 모두 해당 세션 내부에서만 흡수되며, 다른 세션의 처리 흐름에는 전혀 영향을 주지 않습니다.
Text Buffer에 누적된 텍스트가 일정 조건을 만족하면, 세션 워커는 이를 Python 기반 요약 서버로 비동기 전송합니다. 요약 서버는 전달받은 텍스트를 기반으로 주석을 생성한 뒤, 결과를 다시 세션의 Outbound Queue에 적재합니다. 최종적으로 WebSocket Handler는 Outbound Queue에 쌓인 주석 결과를 클라이언트로 푸시하며, 사용자는 화면에서 실시간에 가까운 주석 반영을 확인하게 됩니다.
이 시퀀스에서 중요한 설계 포인트는 모든 단계가 세션 단위로 독립적으로 연결되어 있다는 점입니다. 입력 수집, STT 스트리밍, 텍스트 누적, 요약 요청, 결과 전송까지의 전체 흐름이 하나의 세션 컨텍스트 내부에서 완결되기 때문에, 특정 사용자의 발화 속도나 네트워크 상태, STT 응답 지연이 다른 사용자 경험에 전파되지 않습니다. 이는 단순한 비동기 처리 이상의 의미를 가지며, 실시간 시스템에서 가장 위험한 형태의 병목인 전파형 병목을 구조적으로 차단하는 설계라고 볼 수 있습니다.
이와 같이 SpeakNote의 실시간 주석 생성 시퀀스는 기능 단위가 아니라 흐름 단위로 분리된 구조를 기반으로 설계되었으며, 이를 통해 단일 사용자 환경뿐만 아니라 다중 사용자 환경에서도 안정적인 실시간 처리가 가능하도록 구성되었습니다.
6. 정리하며
SpeakNote의 STT 처리 구조는 단순히 gRPC를 사용했다는 사실보다, 어디에, 어떤 단위로 gRPC 스트림을 배치했는가에 설계의 핵심이 있습니다. 세션 단위로 독립된 STT 스트림을 유지함으로써, 실시간 음성 처리에서 발생할 수 있는 병목과 지연 전파 문제를 구조적으로 차단할 수 있었습니다.
gRPC 자체의 특징이나, Google STT Streaming API의 내부 동작 방식에 대한 자세한 설명은 본 글의 범위를 벗어나므로, 해당 내용은 다음 글에서 별도로 정리할 예정입니다.