リアルタイム通信 — WebSocket・gRPC ストリーミング・WebRTC
gRPC ストリーミングの 4 つの形
한국어 원문으로 표시합니다.
한 줄 요약
gRPC 호출은 HTTP/2 스트림 하나이고, 메시지는 그 위에 길이를 앞에 단 조각으로 흐르며, 상태 코드는 맨 끝 트레일러에 실려 옵니다.
왜 이게 필요했나
단항 호출만으로 실시간 기능을 만들면 폴링이 됩니다. 음성 인식의 부분 결과, LLM 의 토큰, 시세처럼 조금씩 만들어지는 결과를 한 번에 모아 보내면 첫 조각을 받기까지의 시간이 전체 처리 시간과 같아집니다. gRPC 핵심 개념 문서 는 호출을 네 모양으로 나눕니다 — 단항, 서버 스트리밍, 클라이언트 스트리밍, 양방향 스트리밍. 모양은 proto 의 rpc 선언에서 요청과 응답 중 어느 쪽에 stream 을 붙였는지로 정해지고, 생성 코드가 그 모양에 맞는 스텁을 만듭니다.
어떻게 동작하나
gRPC over HTTP/2 명세 를 보면 호출 하나가 HTTP/2 스트림 하나입니다. 요청은 :path /패키지.서비스/메서드, content-type: application/grpc, 기한이 있으면 grpc-timeout 머리로 시작하고, 메시지는 1바이트 압축 표시 + 4바이트 길이 + protobuf 바이트로 이어 붙습니다. 응답도 같은 모양의 메시지 뒤에 트레일러의 grpc-status 와 grpc-message 로 끝납니다.
이 구조에서 세 가지가 나옵니다.
첫째, 스트리밍은 만드는 대로 보낼 때만 스트리밍입니다. 파이썬 서버에서 제너레이터로 yield 하면 gRPC 가 하나씩 꺼내 보냅니다. 결과를 list 에 다 모아 돌려주면 첫 메시지가 마지막 메시지와 같은 시각에 도착합니다. 코드 모양은 스트리밍인데 사용자 경험은 단항입니다.
둘째, 상태는 끝에 옵니다. 서버가 메시지 셋을 보낸 뒤 오류로 끝나면, 클라이언트는 메시지 셋을 받은 다음에야 오류를 봅니다. 파이썬 클라이언트에서는 반복 도중에 grpc.RpcError 가 튀어나옵니다. list(stub.Method(...)) 한 줄로 받으면 이미 받은 셋을 잃습니다. 부분 결과가 의미 있는 스트림이라면 반복문 안에서 하나씩 챙겨야 합니다. 상태 코드는 17가지이고, 실시간 서비스에서 자주 보는 것은 DEADLINE_EXCEEDED(4), CANCELLED(1), UNAVAILABLE(14), RESOURCE_EXHAUSTED(8), ABORTED(10), INVALID_ARGUMENT(3) 입니다.
셋째, 양방향 스트림의 두 방향은 서로 독립입니다. 서버는 요청을 다 받기 전에 답할 수 있고, 핵심 개념 문서의 표현대로 두 스트림은 어떤 순서로든 읽고 쓸 수 있습니다. 그 자유 때문에 서로 기다리다 멈추는 교착이 쉽게 생깁니다. 클라이언트는 답을 받은 뒤에 다음 요청을 보내는데 서버가 list(request_iterator) 로 요청을 먼저 다 모으면, 클라이언트는 답을, 서버는 요청의 끝(half-close)을 기다립니다. 기한이 없으면 영원히 기다립니다.
기한은 호출에 겁니다. 기한 안내 는 기본값이 사실상 무한이므로 모든 호출에 기한을 명시하라고 권합니다. 기한은 grpc-timeout 머리로 서버에 전해지고, 기한이 지나면 클라이언트는 DEADLINE_EXCEEDED 를 받습니다. 그런데 서버의 처리 스레드는 저절로 멈추지 않습니다. 서버 코드가 context.is_active() 나 context.time_remaining() 으로 스스로 확인해야 합니다. 확인하지 않으면 아무도 받지 않을 결과를 위해 CPU 와 DB 연결을 끝까지 씁니다.
메시지 크기에도 한도가 있습니다. 대부분의 구현은 받는 메시지의 기본 상한을 4MB 로 두고, 넘으면 RESOURCE_EXHAUSTED 로 호출을 끝냅니다. 큰 결과를 한 메시지에 담는 대신 스트림으로 잘라 보내는 것이 스트리밍의 또 다른 쓸모입니다. 잘라 보내면 받는 쪽은 첫 조각부터 처리를 시작할 수 있고, 흐름 제어가 한 번에 붙잡는 메모리도 조각 크기로 줄어듭니다. 반대로 조각을 너무 잘게 나누면 메시지마다 붙는 5바이트 머리와 직렬화 비용이 커지므로, 음성처럼 박자가 있는 데이터는 20ms 같은 자연스러운 단위로 자릅니다.
현장에서 만나는 모습
"스트리밍으로 바꿨는데 여전히 한꺼번에 온다" 는 서버가 모아서 보내거나, 중간의 프록시나 로드밸런서가 응답을 버퍼링하는 경우입니다. 둘을 구별하려면 서버 옆에서 한 번, 사용자 쪽에서 한 번 첫 메시지 도착 시각을 재 보면 됩니다. "클라이언트는 타임아웃이 났는데 서버 CPU 가 계속 돈다" 는 기한을 확인하지 않는 서버 코드입니다. 기한 초과가 몰리는 순간 서버는 이미 버려진 일로 가득 차고, 새 요청까지 느려지는 연쇄 장애로 번집니다.
다음 실습에서 할 것
meter.proto 에서 코드를 만들고, 서버·클라이언트·양방향 스트리밍을 구현합니다. 채점기는 첫 메시지의 도착 시각으로 스트리밍 여부를, 한 줄씩 주고받는 기한 3초짜리 호출로 교착 여부를 봅니다. 스트림 도중 오류에서 받은 값을 지키는 클라이언트와, 기한이 지나면 스스로 멈추는 서버까지 만듭니다.