gRPC Streaming으로 상태 전송 받기 예제

gRPC에서 Observer pattern을 구현하는 예제를 고민했던 적이 있었는데(gRPC를 이용한 Observer Pattern) 알고보니 gRPC에서는 ReadReactor / WriteReactor 보다 간단하게 object의 상태 변경을 감지할 수 있는 메커니즘을 제공하고 있었다. gRPC를 이용한 Observer pattern 구현 예제를 찾기 어려웠던 이유가 어쩌면 그런 복잡한 패턴 없이도 구현이 가능하기 때문이었는지도 모르겠다.

이 포스팅에서는 Stream을 통해 변경되는 값들을 subscribe하는 간단한 예제를 Rust로 구현해 본다. gRPC의 Rust 구현체인 tonic과 proto compiler인 prost, 비동기 처리를 위해 tokio를 사용했다.

Proto file – stockprice.proto

랜덤하게 생성된 주가 정보를 gRPC로 구독하는 간단한 서비스 예제를 가정해 보자. Proto file은 UpdateStockPrice()를 정의하고 호출되면 StockPriceResponse의 stream을 받아오도록 다음과 같이 구성한다.

syntax = "proto3";
package stockservice;

import "google/protobuf/empty.proto";

message StockPriceResponse {
    string symbol = 1;
    double price = 2;
    uint64 sequence = 3;
}

service StockService {
    rpc UpdateStockPrice(google.protobuf.Empty) returns (stream StockPriceResponse);
}

Server

StockServiceImpl 구조체를 선언하고 서비스를 제공하기 위한 StockService를 다음과 같이 구현한다.

// Server 구현체
struct StockServiceImpl;

// tokio_stream을 이용해서 gRPC stream을 구현.
type StockStream = Pin<Box<dyn Stream<Item = Result<StockPriceResponse, Status>> + Send>>;

#[tonic::async_trait]
impl StockService for StockServiceImpl {
    type UpdateStockPriceStream = StockStream;

    async fn update_stock_price(
        &self,
        _request: Request<()>,
    ) -> Result<Response<Self::UpdateStockPriceStream>, Status> {

        // 1초 간격으로 stock price 생성
        let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
            Duration::from_secs(1),
        ));

        // Stream으로 전송할 데이터를 생성하고 yield로 반환.
        let output = async_stream::stream! {
            let mut rng = SmallRng::from_os_rng();
            let mut sequence: u64 = 0;

            // 매 interval마다 순번을 증가시키고 랜덤 주가를 생성해 클라이언트로 전송
            while interval.next().await.is_some() {
                sequence += 1;
                let p = generate_stock_price(&mut rng, sequence);
                yield Ok(p);
            }
        };

        Ok(Response::new(
            Box::pin(output) as Self::UpdateStockPriceStream
        ))
    }
}

이 코드에서 핵심은 async_stream::stream!() 매크로로 데이터를 전송할 데이터임을 정의하고, while / yeild로 data를 전송하는 다음의 부분이다. 이 부분에서 데이터를 갱신하고 stream에 지속적으로 update 시킨다.

        let output = async_stream::stream! {
           ...

            // 매 interval마다 순번을 증가시키고 랜덤 주가를 생성해 클라이언트로 전송
            while interval.next().await.is_some() {
                // Data 생성 및 전송
                ...
                yield Ok(p);
            }
        };

Client

Server에 비해 Client 쪽은 비교적 구현이 간단한데, prost가 proto file로 부터 생성해 준 StockServiceClient를 이용해서 stream을 생성하고, 이 때 반환받은 stream으로 부터 계속 데이터를 읽어 주면된다.

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    let mut client = StockServiceClient::connect("http://[::1]:50051").await?;

    // grpc 서비스 stream 생성.
    let mut stream = client
        .update_stock_price(Request::new(()))
        .await?
        .into_inner();

    // stream에서 데이터를 받아서 화면에 출력.
    while let Some(update) = stream.message().await? {
        println!(
            "[{}] {}: {:.2}",
            update.sequence, update.symbol, update.price
        );
    }

    Ok(())
}

코드 위치 및 수행결과

전체 구현 코드는 다음 위치에 있다. https://github.com/litcoder/grpcobsr/tree/stream_rust

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다