gRPC 스트리밍은 프로토콜 자체가 더 빠른 게 아니라, 반복되는 호출·연결·헤더 비용을 하나의 장기 연결로 묶어서 줄이는 방식이다. 이 이점은 "메시지를 계속 주고받는" 조건에서만 성립하고, 단발성 호출이나 Python 같은 런타임에서는 오히려 손해를 볼 수 있다. 판교에서 로그 tailing과 작업 진행률 스트림을 붙여본 경험을 토대로, 언제 쓰고 언제 피해야 하는지 정리했다.
gRPC 스트리밍은 언제 unary보다 나은 선택일까?
결론부터 말하면, 스트리밍은 "데이터가 시간축을 따라 계속 흐르는" 문제에 맞고, "한 번 묻고 한 번 답하는" 문제에는 과한 장비다. gRPC 공식 문서도 장시간 지속되는 클라이언트→서버, 서버→클라이언트, 양방향 흐름에 스트리밍을 고려하라고 권고하며, 매 메시지마다 새 RPC를 시작하지 않아 HTTP/2 요청 생성과 핸들러 호출 비용을 줄일 수 있다고 설명한다 출처: grpc.io.
- 스트리밍의 실질 이점은 "프로토콜이 빠르다"가 아니라 "반복 호출 비용을 제거한다"는 것
- 이 이점은 연결이 이미 열려 있고 메시지가 지속적으로 오가는 조건에서만 성립한다
- 동시 스트림이 많아지면 짧은 unary 호출까지 대기열에 밀릴 수 있다
- Python처럼 언어별로 스트리밍이 더 느려지는 구현도 있다
- 계약은 메시지 순서·종료 의미·재연결까지 포함해야 한다
처음 이 패턴을 붙인 건 배치 작업 진행률 알림이었다. 클라이언트가 5초마다 polling하던 걸 서버 스트리밍으로 바꾸면 호출 수가 줄어들 거라 기대했는데, 실제로 줄어든 건 호출 수지 지연시간 자체는 아니었다. 이 둘을 구분하지 못하면 "스트리밍으로 바꿨는데 왜 안 빨라지냐"는 질문에 답할 수 없다.
스트리밍이 반복 호출보다 빠른 이유는 무엇일까?
핵심은 연결 수립 비용을 한 번만 내고 끝낸다는 데 있다. ASP.NET Core gRPC 문서는 양방향 스트리밍이 여러 번의 unary 호출보다 빠를 수 있다고 설명하는데, 이는 스트림이 시작된 뒤에는 기존 HTTP/2 요청 위에 메시지를 얹어 보내므로 매번 새 HTTP/2 요청을 만드는 오버헤드가 사라지기 때문이다 출처: learn.microsoft.com.
같은 문서는 채널 재사용도 강조한다. 채널을 호출마다 새로 만들면 소켓·TCP·TLS·HTTP/2 핸드셰이크가 매번 반복되어 완료 시간이 크게 늘어날 수 있다. 즉 "스트리밍이 빠르다"의 전제는 사실 "연결 재사용이 빠르다"에 가깝다. unary 호출이라도 채널을 재사용하면 상당 부분의 이점을 가져갈 수 있고, 스트리밍이라도 매번 새 채널을 열면 그 이점이 사라진다. 이 둘을 구분해서 측정하지 않으면 원인을 엉뚱한 곳에서 찾게 된다.
동시 스트림이 늘어나면 왜 짧은 호출까지 느려질까?
HTTP/2는 하나의 연결 위에서 여러 스트림을 다중화하는데, 이 동시성에는 한도가 있다. Microsoft 문서는 HTTP/2 서버가 최대 동시 스트림을 기본값 100개로 설정하는 경우가 많다고 안내한다. 하나의 gRPC 채널에서 활성 RPC가 이 한도에 도달하면 추가 호출은 클라이언트 쪽 대기열에 쌓인다 출처: learn.microsoft.com.
장시간 유지되는 스트리밍 RPC가 많은 시스템에서는 이 한도 때문에 전혀 무거워 보이지 않는 짧은 unary 호출까지 지연될 수 있다. 측정할 때는 active streams, 클라이언트 쪽 queued calls, p95·p99 대기시간을 각각 분리해서 봐야 한다 출처: grpc.io. 그렇다고 동시 스트림 한도를 무작정 올리는 것도 답은 아니다. Microsoft 문서는 단일 연결에 스트림을 과도하게 늘리면 스트림 간 쓰기 경쟁이 생기고, TCP 패킷 손실 한 번으로 그 연결의 여러 호출이 함께 막힐 수 있다고 설명한다. 결국 장기 스트림과 짧은 요청을 같은 채널에 몰아넣지 말고, 트래픽 성격별로 채널을 분리하는 게 실험해볼 가치가 있는 선택지다.
대용량 메시지는 스트리밍에서 어떻게 다뤄야 할까?
메시지 크기와 흐름 제어 윈도는 함께 조정해야 하는 짝이다. ASP.NET Core 문서는 Kestrel의 기본 스트림 윈도 크기로 768KB를 언급하며, 이보다 큰 메시지를 자주 받는다면 연결·스트림 윈도 크기 조정을 검토하라고 안내한다. 연결 윈도는 스트림 윈도 이상이어야 한다 출처: learn.microsoft.com.
대용량 바이너리를 한 메시지에 몰아넣기보다 여러 메시지로 쪼개 스트리밍하는 방식이 권장되는 이유도 여기 있다. 다만 청크 크기를 작게 쪼갤수록 프레이밍·콜백·스케줄링 오버헤드가 늘고, 너무 크게 쪼개면 버퍼링과 메모리 사용량, 첫 바이트 지연이 늘어난다. 공식 자료 어디에도 보편적으로 맞는 청크 크기는 제시되어 있지 않다. 실측할 때는 압축 여부와 청크 크기를 반드시 조건에 포함시켜야 하고, 환경마다 최적값이 달라질 수 있다는 전제를 깔고 가야 한다.
스트리밍이 항상 unary보다 빠르다는 말은 사실일까?
사실이 아니다. 조건부 명제다. gRPC 공식 성능 문서는 스트리밍이 소규모에서는 성능을 높일 수 있지만, 대규모에서는 로드밸런싱 제약과 복잡성 때문에 오히려 확장성을 낮출 수 있다고 명시한다 출처: grpc.io. 또한 스트림이 한번 시작되면 일반 RPC처럼 로드밸런싱할 수 없고, 장애 원인 추적도 더 어려워진다.
언어별 구현 차이도 오해를 만드는 지점이다. 같은 문서는 Python에서 스트리밍 RPC가 수신·송신을 위해 추가 스레드를 만들 수 있어 unary RPC보다 훨씬 느려질 수 있다고 명시한다. 다른 언어에서 얻은 결과를 Python 서비스에 그대로 적용하면 안 된다. 벤치마크를 인용할 때는 언어·런타임 버전, gRPC 라이브러리 버전, 메시지 크기, 동시 스트림 수, 채널 재사용 여부, TLS 사용 여부를 함께 적지 않으면 그 수치는 다른 환경에서 재현되지 않는다.
언제 쓰고 언제 피해야 할까?
적합한 조건은 데이터가 시간축을 따라 계속 도착하거나, 요청과 응답이 장시간 병행되거나, 대용량 데이터를 청크 단위로 다뤄야 할 때다. Android 공식 문서도 대용량 업로드·다운로드와 클라이언트·서버가 동시에 읽고 쓰는 상호작용을 스트리밍의 대표 용도로 제시한다 출처: developer.android.com. 로그 tailing, 실시간 상태 변경, 파일 청크 전송, 장시간 작업 진행률, 양방향 세션이 여기 해당한다.
반대로 피해야 할 조건은 단발성 조회나 화면 조합이다. 외부·브라우저 API에서 캐시와 프록시 도구를 쓰고 싶다면 REST가 유리하고, 화면별로 필요한 필드가 자주 달라진다면 GraphQL이 오버페칭 문제를 줄여준다. 내부 서비스 간 명확한 메서드 계약과 코드 생성, 낮은 직렬화 오버헤드가 필요하지만 데이터 흐름이 한 번으로 끝난다면 gRPC unary로 충분하다. 실제로는 한 제품 안에서 외부 API는 REST/GraphQL, 내부 단발 호출은 gRPC unary, 실시간·대용량 경로만 gRPC streaming으로 나누는 혼용이 더 흔하다.
스트리밍을 선택했다면 계약에 넣어야 할 항목도 늘어난다. 필드 호환성 검사만으로는 부족하다. sequence_id나 이벤트 버전, 재연결 기준, 중복 메시지 허용 여부와 멱등성 키, 정상 종료와 오류 종료의 구분, 서버가 클라이언트보다 빠를 때의 backpressure 정책, 최대 메시지 크기와 타임아웃을 명시해야 한다. 예를 들어 event_type을 하나 추가했는데 소비자가 모르는 이벤트를 무시하지 못하면, Protobuf 자체는 호환되어도 스트림 전체가 죽을 수 있다. 운영 중에는 스트림별 연결 지속시간, 종료 코드, 재연결 횟수, 메시지 처리 지연, 버퍼 깊이를 따로 기록해야 하고, "연결은 살아 있지만 데이터가 멈춘" 상태는 heartbeat로 별도 감지해야 한다. 일반 HTTP 요청의 성공률만 보고 있으면 이 상태를 놓친다.
2026년 기준으로도 이 판단 기준 자체는 크게 바뀌지 않았다. 바뀌는 건 각 조건에서의 구체적인 임계값뿐이고, 그 임계값은 동일한 데이터 요구를 단발 호출, 반복 호출, 장기 스트림, 대용량 청크 전송으로 나눠 직접 재보는 수밖에 없다.
핵심 정리
- gRPC 스트리밍의 이점은 "프로토콜이 빠르다"가 아니라 반복 호출·연결·헤더 비용을 하나의 장기 연결로 줄이는 것
- HTTP/2 동시 스트림 기본 한도(흔히 100개 수준)에 걸리면 짧은 unary 호출까지 대기열에 밀릴 수 있다
- 소규모에서는 이점이 있지만 대규모에서는 로드밸런싱 제약 때문에 확장성이 낮아질 수 있다
- Python 등 일부 언어는 스트리밍이 unary보다 느려질 수 있어 벤치마크를 다른 언어에 그대로 적용하면 안 된다
- 스트리밍 계약에는 필드 호환성 외에 순서·종료·재연결·backpressure까지 포함해야 한다