HTTP/2 기반 gRPC의 4가지 스트리밍 모드, Protobuf 직렬화, 마이크로서비스 간 추론 파이프라인 구현을 다룹니다. gRPC-Web의 제약과 Python/Go 구현 예제를 포함합니다.
2장과 3장에서 다룬 SSE와 WebSocket은 주로 클라이언트(브라우저)와 서버 간 통신에 초점을 맞추었습니다. 그러나 실제 AI 시스템의 백엔드는 여러 마이크로서비스로 구성됩니다.
API Gateway에서 추론 라우터로, 추론 라우터에서 GPU 서버로 데이터를 전달할 때, HTTP REST API를 사용할 수도 있습니다. 하지만 다음과 같은 문제가 있습니다.
gRPC는 Google이 개발한 고성능 RPC 프레임워크로, 이 모든 문제를 해결합니다.
gRPC는 HTTP/2 위에서 동작합니다. HTTP/2의 핵심 특성인 **멀티플렉싱(Multiplexing)**은 하나의 TCP 연결 위에서 여러 요청/응답을 동시에 처리할 수 있게 해줍니다.
[HTTP/1.1]
연결 1: 요청A ──────> 응답A
연결 2: 요청B ──────> 응답B (별도 연결 필요)
연결 3: 요청C ──────> 응답C
[HTTP/2 멀티플렉싱]
연결 1: 스트림1(요청A) ──> 스트림2(요청B) ──> 스트림1(응답A)
스트림3(요청C) ──> 스트림2(응답B) ──> 스트림3(응답C)
(하나의 연결에서 동시 처리)gRPC는 기본 직렬화 형식으로 **Protobuf(Protocol Buffers)**를 사용합니다. JSON 대비 직렬화 크기가 3-10배 작고, 처리 속도는 10-100배 빠릅니다.
syntax = "proto3";
package inference;
service InferenceService {
// 단항 RPC
rpc Predict(PredictRequest) returns (PredictResponse);
// 서버 스트리밍 RPC
rpc StreamPredict(PredictRequest) returns (stream TokenResponse);
// 클라이언트 스트리밍 RPC
rpc StreamInput(stream AudioChunk) returns (TranscriptResponse);
// 양방향 스트리밍 RPC
rpc Converse(stream ConversationInput) returns (stream ConversationOutput);
}
message PredictRequest {
string model_id = 1;
string prompt = 2;
int32 max_tokens = 3;
float temperature = 4;
}
message TokenResponse {
string token = 1;
int32 index = 2;
bool is_finished = 3;
TokenMetadata metadata = 4;
}
message TokenMetadata {
float log_probability = 1;
int64 latency_ms = 2;
}
message AudioChunk {
bytes data = 1;
int32 sample_rate = 2;
string encoding = 3;
}Protobuf는 필드 번호 기반으로 직렬화합니다. 필드를 추가할 때 기존 번호를 재사용하지 않으면 하위 호환성이 유지됩니다. 이는 마이크로서비스의 독립 배포에 핵심적인 특성입니다.
gRPC의 차별점은 4가지 통신 모드를 지원한다는 것입니다.
일반적인 요청-응답 패턴입니다. REST API와 유사하지만, Protobuf와 HTTP/2의 성능 이점을 누립니다.
클라이언트 ── 요청 ──> 서버
클라이언트 <── 응답 ── 서버클라이언트가 하나의 요청을 보내면, 서버가 스트림으로 여러 응답을 보냅니다. LLM 토큰 스트리밍에 가장 적합한 모드입니다.
클라이언트 ── 요청 ──> 서버
클라이언트 <── 토큰1 ── 서버
클라이언트 <── 토큰2 ── 서버
클라이언트 <── 토큰3 ── 서버
클라이언트 <── 완료 ── 서버클라이언트가 여러 메시지를 스트림으로 보내고, 서버가 하나의 응답을 반환합니다. 실시간 음성 입력에서 오디오 청크를 연속 전송할 때 사용됩니다.
클라이언트 ── 오디오청크1 ──> 서버
클라이언트 ── 오디오청크2 ──> 서버
클라이언트 ── 오디오청크3 ──> 서버
클라이언트 <── 전체 인식 결과 ── 서버양쪽이 동시에 스트림을 보냅니다. 실시간 음성 대화(사용자 발화 + AI 응답이 동시에 진행)에서 사용됩니다.
Python에서 gRPC 서버 스트리밍을 구현하는 예제입니다.
import grpc
from concurrent import futures
import inference_pb2
import inference_pb2_grpc
class InferenceServicer(inference_pb2_grpc.InferenceServiceServicer):
"""gRPC 추론 서비스 구현"""
def StreamPredict(self, request, context):
"""서버 스트리밍: 토큰 단위로 응답"""
model_id = request.model_id
prompt = request.prompt
# 모델 로드 및 추론
model = get_model(model_id)
generator = model.generate(
prompt,
max_tokens=request.max_tokens,
temperature=request.temperature,
)
for index, (token, log_prob) in enumerate(generator):
# 클라이언트가 연결을 끊었는지 확인
if context.is_active():
yield inference_pb2.TokenResponse(
token=token,
index=index,
is_finished=False,
metadata=inference_pb2.TokenMetadata(
log_probability=log_prob,
latency_ms=get_latency_ms(),
),
)
else:
# 클라이언트가 취소함
return
# 완료 신호
yield inference_pb2.TokenResponse(
token="",
index=-1,
is_finished=True,
)
def Converse(self, request_iterator, context):
"""양방향 스트리밍: 실시간 대화"""
for request in request_iterator:
if not context.is_active():
return
# 입력 처리 및 응답 생성
response = process_input(request)
for token in response:
yield inference_pb2.ConversationOutput(
text=token,
speaker="assistant",
)
def serve():
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
options=[
("grpc.max_send_message_length", 50 * 1024 * 1024),
("grpc.max_receive_message_length", 50 * 1024 * 1024),
("grpc.keepalive_time_ms", 30000),
("grpc.keepalive_timeout_ms", 10000),
],
)
inference_pb2_grpc.add_InferenceServiceServicer_to_server(
InferenceServicer(), server
)
server.add_insecure_port("[::]:50051")
server.start()
server.wait_for_termination()Go에서 gRPC 클라이언트로 여러 추론 서버를 라우팅하는 예제입니다.
package main
import (
"context"
"io"
"log"
pb "inference/proto"
"google.golang.org/grpc"
)
type InferenceRouter struct {
pb.UnimplementedInferenceServiceServer
backends []pb.InferenceServiceClient
}
func (r *InferenceRouter) StreamPredict(
req *pb.PredictRequest,
stream pb.InferenceService_StreamPredictServer,
) error {
// 최적의 백엔드 선택 (로드 기반)
backend := r.selectBackend(req.ModelId)
// 백엔드에 스트리밍 요청
ctx := stream.Context()
backendStream, err := backend.StreamPredict(ctx, req)
if err != nil {
return err
}
// 백엔드 응답을 클라이언트에 중계
for {
token, err := backendStream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if err := stream.Send(token); err != nil {
return err
}
}
}
func (r *InferenceRouter) selectBackend(
modelId string,
) pb.InferenceServiceClient {
// 라운드 로빈, 최소 연결, GPU 사용률 기반 등
// 실제 구현은 모니터링 메트릭에 기반
return r.backends[0]
}gRPC는 HTTP/2의 저수준 기능을 활용하기 때문에, 브라우저에서 직접 사용할 수 없습니다. 브라우저의 fetch API가 HTTP/2 프레임에 대한 세밀한 제어를 제공하지 않기 때문입니다.
gRPC-Web은 이 문제를 해결하기 위한 프록시 기반 접근법입니다.
| 한계 | 설명 |
|---|---|
| 프록시 필요 | Envoy 등 별도 프록시 서버 운영 필수 |
| 오버헤드 | 30-40% 직렬화/역직렬화 오버헤드 |
| 스트리밍 제한 | 서버 스트리밍만 지원 (양방향 불가) |
| 생태계 | 클라이언트 라이브러리 성숙도가 낮음 |
프론트엔드-백엔드 간 통신에 gRPC-Web을 도입하는 것은 대부분의 경우 과도한 복잡성을 추가합니다. LLM 토큰 스트리밍에는 SSE가, 양방향 통신에는 WebSocket이 더 실용적입니다. gRPC는 백엔드 서비스 간 통신에 집중하는 것이 바람직합니다.
동일한 추론 결과를 전송할 때의 프로토콜별 성능 차이를 비교합니다.
| 지표 | REST (JSON) | gRPC (Protobuf) | 개선율 |
|---|---|---|---|
| 직렬화 크기 | 1.0x (기준) | 0.3-0.5x | 50-70% 감소 |
| 직렬화 속도 | 1.0x | 5-10x | 5-10배 빠름 |
| 지연시간 (P99) | 15ms | 3ms | 5배 감소 |
| 처리량 | 10,000 req/s | 40,000 req/s | 4배 증가 |
이 수치는 마이크로서비스 간 통신이 빈번한 추론 파이프라인에서 누적 효과가 큽니다. 하나의 사용자 요청이 내부적으로 5-10번의 서비스 간 호출을 발생시킨다면, 각 호출의 지연시간 감소가 전체 응답 시간에 직접적으로 반영됩니다.
실제 AI 시스템에서 gRPC가 어떻게 활용되는지, 전체 파이프라인을 살펴보겠습니다.
각 구간의 프로토콜 선택 근거는 다음과 같습니다.
gRPC 서비스 정의(.proto 파일)는 API 계약서 역할을 합니다. 프론트엔드, 백엔드, ML 팀이 동일한 .proto 파일을 공유하면, 각 언어에 맞는 클라이언트/서버 코드가 자동 생성됩니다. 이는 팀 간 커뮤니케이션 오류를 크게 줄입니다.
이번 장에서는 백엔드 서비스 간 고성능 통신을 위한 gRPC Streaming을 살펴보았습니다.
다음 장에서는 프로토콜 레벨을 넘어, LLM 응답을 실제로 어떻게 처리하고 렌더링하는지를 다룹니다. OpenAI, Anthropic, Google의 스트리밍 API 차이, 구조화된 출력의 파셜 파싱, 그리고 Vercel AI SDK를 활용한 React 스트리밍 UI 구현을 살펴보겠습니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
OpenAI, Anthropic, Google의 스트리밍 API 차이를 비교하고, 구조화된 출력의 파셜 파싱, React 스트리밍 UI 렌더링, Vercel AI SDK 활용법을 다룹니다.
WebSocket의 핸드셰이크, 프레이밍 구조, 양방향 통신의 강점과 상태 관리의 복잡성을 분석합니다. AI 채팅에서의 생성 중단, Socket.IO, 스케일링 전략을 다룹니다.
vLLM의 스트리밍 입력, Continuous Batching, 시맨틱 캐싱, 추론 라우터, 멀티모달 실시간 처리 등 백엔드 추론 파이프라인의 핵심 아키텍처를 다룹니다.